From a0f7930949747e687bcf5a0d69db8b7d1f5d441e Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:32:02 +0200 Subject: [PATCH 001/106] G2B-S1: cli_interactive_validation spawns the cargo-built ruchy binary - ruchy_binary() hard-coded $CARGO_MANIFEST_DIR/target/debug/ruchy, which does not exist when CARGO_TARGET_DIR points elsewhere (NotFound / EOF on 14 tests) - now env!("CARGO_BIN_EXE_ruchy"); 14 passed, 2 ignored (pre-existing) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/cli_interactive_validation.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/cli_interactive_validation.rs b/tests/cli_interactive_validation.rs index 5a47da2a7..6ca24fcd3 100644 --- a/tests/cli_interactive_validation.rs +++ b/tests/cli_interactive_validation.rs @@ -18,10 +18,9 @@ use tempfile::TempDir; // HELPER FUNCTIONS // ============================================================================ -/// Get path to ruchy binary +/// Get path to the cargo-built ruchy binary (honours `CARGO_TARGET_DIR`) fn ruchy_binary() -> String { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - format!("{manifest_dir}/target/debug/ruchy") + env!("CARGO_BIN_EXE_ruchy").to_string() } /// Create temp directory From 68e8d5fe71a8b82e973da450b019e7a96b25a448 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:34:57 +0200 Subject: [PATCH 002/106] G2B-T1: module::function() callees keep :: in transpiled Rust (PARSER-094, Issue #137) A `a::b::c(..)` call parses as Call{FieldAccess} (`obj.m(..)` is a MethodCall), so the callee of a Call that is a lowercase-rooted FieldAccess chain is rendered as a Rust path. self and std roots keep their existing handling. Fixes 6 tests in tests/parser_094_path_separator.rs and tests/parser_094_property_tests.rs. Differential: mod helpers { pub fun double } + helpers::double(4): ruchy run = rustc binary = 8. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2816 +++++++++++------- src/backend/transpiler/call_transpilation.rs | 38 + 2 files changed, 1770 insertions(+), 1084 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 61648ed1a..eb5005729 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-23T14:08:18.278349727Z", + "created_at": "2026-09-24T00:02:05.178092247Z", "git_context": null, "files": { "./build.rs": { @@ -1334,48 +1334,48 @@ }, "./ruchy-embed/src/lib.rs": { "content_hash": [ - 182, + 191, + 153, + 206, + 223, + 218, 172, - 9, - 144, - 64, - 168, - 166, - 252, - 142, - 215, - 221, - 36, - 161, - 204, + 23, 97, - 160, - 119, - 118, - 121, - 78, - 169, - 166, - 237, - 148, - 212, - 22, - 252, - 240, + 25, + 67, + 114, + 162, + 203, + 157, + 5, + 149, 54, - 46, - 174, - 133 + 161, + 3, + 242, + 62, + 176, + 237, + 73, + 151, + 70, + 34, + 93, + 38, + 214, + 74, + 153 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.336245, + "duplication_ratio": 17.391304, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.03295, + "total": 93.08301, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -1391,11 +1391,11 @@ }, { "source_metric": "Duplication", - "amount": 2.6637554, + "amount": 2.6086955, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.3%" + "issue": "Code duplication: 13.0%" } ], "critical_defects_count": 0, @@ -2432,48 +2432,48 @@ }, "./src/backend/compiler.rs": { "content_hash": [ - 47, - 42, - 157, - 120, - 86, - 178, - 224, - 81, + 83, + 156, + 162, + 40, + 232, 231, + 196, + 254, + 16, + 191, + 22, + 26, + 9, + 245, + 237, + 199, + 5, + 161, + 104, + 171, + 117, + 219, + 127, + 167, + 13, + 180, + 90, 68, - 145, - 202, - 135, - 30, - 169, - 48, - 38, - 112, - 137, - 21, - 147, - 96, - 83, - 203, - 80, - 19, - 208, - 96, - 128, - 119, - 59, - 90 + 248, + 6, + 141, + 186 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.601307, + "duplication_ratio": 16.601732, "coupling_score": 14.6, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.20131, + "total": 76.20173, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -2485,11 +2485,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 81 duplicate code patterns" + "issue": "Found 83 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.3986928, + "amount": 3.3982685, "applied_to": [ "Duplication" ], @@ -5317,38 +5317,38 @@ }, "./src/backend/transpiler/expressions.rs": { "content_hash": [ - 88, - 174, - 107, - 225, + 90, + 75, + 96, + 171, + 202, + 232, + 119, + 123, + 162, 59, - 220, - 250, - 19, - 170, - 7, - 136, - 103, - 158, - 53, - 102, - 152, - 76, - 243, - 34, - 47, - 12, - 39, - 184, - 73, - 220, - 222, - 7, + 125, + 248, + 37, + 116, + 65, + 225, 184, - 237, - 172, - 40, - 150 + 228, + 221, + 174, + 167, + 174, + 8, + 0, + 206, + 81, + 12, + 245, + 158, + 29, + 28, + 107 ], "score": { "structural_complexity": 0.0, @@ -5570,48 +5570,48 @@ }, "./src/backend/transpiler/expressions_helpers/binary_ops.rs": { "content_hash": [ - 87, - 7, - 145, - 52, - 194, - 48, - 112, - 236, - 241, - 26, - 133, - 27, - 54, - 186, - 149, - 21, - 122, - 38, - 30, - 234, - 66, - 123, + 239, + 80, 182, - 159, - 208, + 245, + 227, + 243, + 160, + 204, + 196, + 191, + 110, + 213, + 163, + 36, + 119, + 107, + 144, 203, - 129, - 246, - 121, - 85, - 210, - 180 + 130, + 108, + 68, + 33, + 139, + 168, + 141, + 242, + 43, + 158, + 220, + 107, + 244, + 181 ], "score": { "structural_complexity": 4.6, "semantic_complexity": 20.0, - "duplication_ratio": 16.93166, + "duplication_ratio": 16.989246, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 81.53166, + "total": 81.58925, "grade": "B+", "confidence": 1.0, "language": "Rust", @@ -5623,15 +5623,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 42 duplicate code patterns" + "issue": "Found 44 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.0683403, + "amount": 3.0107527, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.3%" + "issue": "Code duplication: 15.1%" }, { "source_metric": "StructuralComplexity", @@ -5665,38 +5665,38 @@ }, "./src/backend/transpiler/expressions_helpers/collections.rs": { "content_hash": [ - 236, - 209, + 87, + 155, + 50, + 220, + 212, + 55, + 179, + 154, + 185, + 185, + 232, + 83, + 219, + 194, + 91, + 237, + 20, + 133, + 185, + 229, + 80, + 12, 63, - 81, 30, - 35, - 151, - 150, - 10, - 139, - 175, - 7, - 62, + 60, 137, - 47, - 231, - 179, - 63, - 94, - 132, - 124, - 23, - 248, - 195, - 179, - 22, - 255, - 49, - 122, - 194, - 198, - 215 + 182, + 113, + 161, + 161, + 58, + 0 ], "score": { "structural_complexity": 16.9, @@ -9711,38 +9711,38 @@ }, "./src/bench/mod.rs": { "content_hash": [ - 105, - 149, - 47, - 133, - 138, - 84, - 106, - 214, - 115, - 148, - 164, - 65, - 87, - 124, - 101, - 198, - 165, - 17, - 147, + 192, + 56, + 75, + 33, + 13, + 125, + 0, + 241, 121, - 163, - 51, - 52, - 203, - 94, - 123, - 134, - 1, - 106, - 143, + 241, + 63, + 132, + 190, + 154, 62, - 174 + 102, + 203, + 169, + 126, + 149, + 250, + 77, + 12, + 213, + 224, + 77, + 64, + 145, + 112, + 68, + 101, + 45 ], "score": { "structural_complexity": 25.0, @@ -11901,48 +11901,48 @@ }, "./src/bin/handlers/mcp_handler.rs": { "content_hash": [ - 204, - 28, - 67, - 219, - 46, - 151, - 221, - 126, - 78, - 198, - 241, - 92, - 37, - 140, + 35, + 53, + 27, + 13, + 127, + 235, + 244, 172, - 86, - 122, - 229, - 38, - 241, - 31, + 5, 60, - 179, - 143, - 100, - 237, - 124, + 206, + 134, + 18, + 190, + 185, + 27, + 175, + 141, + 183, + 82, + 145, + 63, + 85, + 153, + 45, + 111, + 154, + 17, + 32, 140, - 214, - 61, - 177, - 91 + 150, + 245 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 19.0, - "duplication_ratio": 20.0, + "duplication_ratio": 16.774193, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 94.545456, + "total": 91.61291, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -11954,7 +11954,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 19 duplicate code patterns" + "issue": "Found 14 duplicate code patterns" + }, + { + "source_metric": "Duplication", + "amount": 3.2258062, + "applied_to": [ + "Duplication" + ], + "issue": "Code duplication: 16.1%" }, { "source_metric": "SemanticComplexity", @@ -12897,38 +12905,38 @@ }, "./src/bin/handlers/rhl_handler.rs": { "content_hash": [ - 31, - 177, - 224, - 170, - 148, - 1, - 43, - 252, - 160, + 44, + 34, + 166, + 122, + 81, 28, - 87, - 217, - 220, - 140, - 144, - 219, - 2, - 183, - 250, - 141, - 82, + 213, + 251, + 166, + 202, 50, - 71, - 188, - 124, - 235, - 45, - 159, - 105, - 58, + 182, + 158, + 35, + 239, + 34, + 16, 111, - 83 + 113, + 95, + 179, + 145, + 7, + 189, + 38, + 82, + 116, + 232, + 174, + 168, + 10, + 140 ], "score": { "structural_complexity": 25.0, @@ -12937,8 +12945,8 @@ "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, - "entropy_score": 8.5, - "total": 98.63637, + "entropy_score": 7.0, + "total": 97.27273, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -12946,11 +12954,11 @@ "penalties_applied": [ { "source_metric": "Duplication", - "amount": 1.5, + "amount": 3.0, "applied_to": [ "Duplication" ], - "issue": "Found 3 duplicate code patterns" + "issue": "Found 6 duplicate code patterns" } ], "critical_defects_count": 0, @@ -13482,48 +13490,48 @@ }, "./src/bin/ruchy.rs": { "content_hash": [ - 243, - 156, - 49, - 252, - 14, - 254, - 123, - 238, - 150, + 89, + 161, + 63, + 111, + 200, 58, - 192, - 97, - 197, - 64, - 171, - 3, - 91, - 2, - 80, - 178, - 223, - 235, + 172, + 166, + 210, + 141, + 242, + 228, + 51, + 40, + 49, 45, - 74, - 108, - 131, - 27, - 215, - 64, - 35, - 93, - 161 + 120, + 65, + 32, + 37, + 209, + 143, + 214, + 209, + 15, + 147, + 85, + 233, + 21, + 1, + 149, + 239 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 18.0, - "duplication_ratio": 14.036585, + "duplication_ratio": 14.078947, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 72.03658, + "total": 72.07895, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -13535,15 +13543,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 168 duplicate code patterns" + "issue": "Found 173 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.963415, + "amount": 5.921053, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.8%" + "issue": "Code duplication: 29.6%" }, { "source_metric": "StructuralComplexity", @@ -13551,7 +13559,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 78" + "issue": "High cognitive complexity: 80" }, { "source_metric": "StructuralComplexity", @@ -13559,7 +13567,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 101" + "issue": "High cyclomatic complexity: 106" }, { "source_metric": "SemanticComplexity", @@ -16842,48 +16850,48 @@ }, "./src/frontend/parser/expressions_helpers/patterns.rs": { "content_hash": [ - 32, - 91, - 24, - 179, + 54, + 111, + 107, + 78, + 69, + 199, + 40, + 161, + 55, 208, - 164, - 135, - 171, - 89, - 102, - 210, - 186, + 36, + 93, + 111, + 105, + 122, + 43, 68, - 186, - 44, - 108, - 217, - 246, - 228, - 137, - 174, - 119, - 203, - 206, - 223, - 9, + 79, + 43, + 178, 238, - 105, - 242, - 117, - 65, - 127 + 237, + 182, + 215, + 26, + 122, + 136, + 1, + 19, + 153, + 174, + 82 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 19.0, - "duplication_ratio": 16.032389, + "duplication_ratio": 16.0364, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.03239, + "total": 75.0364, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -16899,7 +16907,7 @@ }, { "source_metric": "Duplication", - "amount": 3.9676113, + "amount": 3.9635994, "applied_to": [ "Duplication" ], @@ -16919,7 +16927,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 171" + "issue": "High cyclomatic complexity: 172" }, { "source_metric": "SemanticComplexity", @@ -18882,38 +18890,38 @@ }, "./src/frontend/parser/utils_helpers/string_interpolation.rs": { "content_hash": [ - 197, - 187, + 177, + 26, + 145, + 48, + 114, + 26, 167, - 59, - 80, - 152, - 98, - 139, + 191, + 6, + 170, + 83, + 9, 19, - 150, - 177, - 233, - 108, - 205, - 21, - 56, - 198, - 56, - 130, - 41, - 94, - 97, - 158, - 195, - 146, - 80, - 69, + 115, + 73, + 102, 159, - 142, - 240, - 169, - 165 + 127, + 28, + 69, + 112, + 38, + 231, + 202, + 203, + 71, + 251, + 139, + 212, + 17, + 100, + 55 ], "score": { "structural_complexity": 20.3, @@ -20822,6 +20830,77 @@ }, "git_context": null }, + "./src/mcp/rhl_tools.rs": { + "content_hash": [ + 62, + 74, + 163, + 104, + 209, + 199, + 229, + 216, + 81, + 169, + 60, + 27, + 102, + 83, + 77, + 58, + 105, + 60, + 208, + 63, + 192, + 253, + 245, + 181, + 98, + 36, + 247, + 27, + 53, + 201, + 63, + 132 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 95.454544, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/mcp/rhl_tools.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 11 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/mcp/tool_discovery.rs": { "content_hash": [ 250, @@ -20911,38 +20990,38 @@ }, "./src/mcp.rs": { "content_hash": [ - 217, - 224, - 220, - 85, - 184, - 30, - 184, - 4, - 49, - 82, - 59, - 199, - 200, - 249, - 190, - 79, - 96, - 156, - 253, - 138, - 68, - 107, - 90, - 11, + 135, + 215, + 251, + 50, + 22, + 194, 19, - 253, - 137, - 3, - 169, - 75, - 151, - 43 + 5, + 131, + 166, + 84, + 66, + 47, + 140, + 46, + 172, + 154, + 66, + 86, + 252, + 150, + 24, + 179, + 78, + 49, + 36, + 251, + 217, + 103, + 78, + 191, + 119 ], "score": { "structural_complexity": 25.0, @@ -20964,7 +21043,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 36 duplicate code patterns" + "issue": "Found 34 duplicate code patterns" } ], "critical_defects_count": 0, @@ -27673,38 +27752,38 @@ }, "./src/quality/formatter_config.rs": { "content_hash": [ - 224, - 145, - 198, - 147, - 201, - 131, - 129, - 58, - 33, - 72, - 6, - 105, - 187, - 211, - 56, - 98, - 91, - 93, - 52, - 179, - 42, - 218, + 252, 179, - 106, - 123, + 135, + 66, 2, - 81, - 163, - 210, - 142, - 244, - 95 + 52, + 175, + 172, + 130, + 156, + 160, + 5, + 189, + 83, + 150, + 62, + 149, + 30, + 246, + 127, + 255, + 54, + 121, + 141, + 232, + 214, + 74, + 205, + 104, + 152, + 38, + 38 ], "score": { "structural_complexity": 25.0, @@ -29902,38 +29981,38 @@ }, "./src/rhl/check.rs": { "content_hash": [ - 207, - 62, - 191, + 68, + 228, + 168, + 155, + 232, + 157, + 114, + 168, 87, - 206, - 124, - 52, - 230, - 81, + 233, + 1, + 56, + 46, + 58, + 31, + 178, + 39, + 205, 34, - 147, - 3, - 28, - 242, - 136, - 244, - 138, - 193, - 249, - 24, + 41, + 153, + 170, + 246, + 13, + 33, + 110, + 179, + 209, 196, - 38, - 200, - 253, - 9, - 104, - 69, - 78, - 219, - 35, - 57, - 23 + 155, + 185, + 42 ], "score": { "structural_complexity": 23.2, @@ -29955,7 +30034,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 10 duplicate code patterns" + "issue": "Found 14 duplicate code patterns" }, { "source_metric": "StructuralComplexity", @@ -29981,49 +30060,49 @@ }, "./src/rhl/cli.rs": { "content_hash": [ - 221, - 154, - 38, - 188, - 220, - 229, + 158, + 144, + 241, + 223, + 92, + 202, + 131, + 169, + 254, + 130, + 238, + 208, 139, - 110, - 31, - 207, - 54, - 220, - 148, - 45, - 51, - 137, - 145, - 48, - 240, - 78, - 150, - 140, - 227, - 120, - 232, - 26, - 166, + 218, + 88, + 118, + 204, + 179, + 253, + 73, 150, - 241, - 194, - 44, - 122 + 76, + 134, + 117, + 86, + 104, + 41, + 230, + 126, + 243, + 28, + 138 ], "score": { - "structural_complexity": 22.0, + "structural_complexity": 0.0, "semantic_complexity": 20.0, "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.72727, - "grade": "A", + "total": 80.0, + "grade": "B+", "confidence": 1.0, "language": "Rust", "file_path": "./src/rhl/cli.rs", @@ -30034,23 +30113,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 21 duplicate code patterns" + "issue": "Found 47 duplicate code patterns" }, { "source_metric": "StructuralComplexity", - "amount": 1.5, + "amount": 10.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 20" + "issue": "High cognitive complexity: 50" }, { "source_metric": "StructuralComplexity", - "amount": 1.5, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 33" + "issue": "High cyclomatic complexity: 81" } ], "critical_defects_count": 0, @@ -30068,38 +30147,38 @@ }, "./src/rhl/codes.rs": { "content_hash": [ + 35, 238, - 251, - 57, - 42, - 212, - 151, - 243, - 88, - 2, - 69, - 155, - 241, - 135, + 184, + 51, + 254, + 25, + 147, 74, - 88, + 218, + 47, + 68, + 186, 163, - 127, - 196, - 191, - 6, + 80, + 141, + 159, + 116, + 48, + 242, + 192, + 143, + 165, + 238, + 96, + 118, 7, - 36, - 33, - 237, - 42, - 218, - 255, - 111, - 107, - 198, + 144, + 164, + 250, 231, - 55 + 8, + 158 ], "score": { "structural_complexity": 25.0, @@ -30108,8 +30187,8 @@ "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, - "entropy_score": 8.5, - "total": 98.63637, + "entropy_score": 7.5, + "total": 97.72727, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -30117,11 +30196,169 @@ "penalties_applied": [ { "source_metric": "Duplication", - "amount": 1.5, + "amount": 2.5, "applied_to": [ "Duplication" ], - "issue": "Found 3 duplicate code patterns" + "issue": "Found 5 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/contract.rs": { + "content_hash": [ + 158, + 135, + 143, + 79, + 221, + 80, + 51, + 214, + 62, + 212, + 215, + 105, + 91, + 243, + 115, + 29, + 17, + 183, + 233, + 246, + 159, + 29, + 97, + 124, + 30, + 123, + 162, + 116, + 179, + 112, + 59, + 224 + ], + "score": { + "structural_complexity": 6.1, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 86.1, + "grade": "A-", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/contract.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 17 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 3.9, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 28" + }, + { + "source_metric": "StructuralComplexity", + "amount": 15.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 73" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/contract_render.rs": { + "content_hash": [ + 179, + 1, + 7, + 12, + 3, + 171, + 37, + 125, + 142, + 171, + 242, + 39, + 50, + 37, + 254, + 249, + 202, + 124, + 161, + 95, + 131, + 229, + 207, + 36, + 113, + 221, + 106, + 226, + 147, + 181, + 69, + 56 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 95.454544, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/contract_render.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 14 duplicate code patterns" } ], "critical_defects_count": 0, @@ -30208,6 +30445,180 @@ }, "git_context": null }, + "./src/rhl/examples.rs": { + "content_hash": [ + 73, + 69, + 222, + 222, + 221, + 232, + 105, + 129, + 217, + 154, + 126, + 150, + 228, + 130, + 244, + 209, + 166, + 96, + 75, + 86, + 57, + 174, + 137, + 196, + 74, + 138, + 106, + 132, + 41, + 23, + 212, + 157 + ], + "score": { + "structural_complexity": 12.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 92.0, + "grade": "A", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/examples.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 27 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 6.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 35" + }, + { + "source_metric": "StructuralComplexity", + "amount": 7.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 44" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/explain.rs": { + "content_hash": [ + 221, + 48, + 84, + 65, + 152, + 112, + 60, + 137, + 143, + 205, + 1, + 10, + 7, + 66, + 108, + 56, + 24, + 120, + 232, + 51, + 166, + 100, + 157, + 140, + 134, + 251, + 193, + 240, + 103, + 166, + 110, + 186 + ], + "score": { + "structural_complexity": 13.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 93.0, + "grade": "A", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/explain.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 15 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 3.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 25" + }, + { + "source_metric": "StructuralComplexity", + "amount": 9.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 48" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/rhl/fix.rs": { "content_hash": [ 217, @@ -30289,38 +30700,38 @@ }, "./src/rhl/fmt.rs": { "content_hash": [ - 47, - 28, - 196, - 39, - 208, - 229, - 187, - 99, - 9, - 228, - 30, - 220, - 163, - 169, - 141, - 4, - 171, - 202, - 98, - 201, + 100, + 243, + 222, + 164, + 145, + 162, + 65, + 130, 234, - 58, - 228, - 52, - 179, - 25, - 5, - 17, - 155, - 179, - 133, - 181 + 76, + 243, + 92, + 144, + 169, + 139, + 120, + 95, + 159, + 21, + 177, + 218, + 253, + 82, + 181, + 92, + 198, + 73, + 200, + 47, + 206, + 69, + 81 ], "score": { "structural_complexity": 8.1, @@ -30374,40 +30785,127 @@ }, "git_context": null }, - "./src/rhl/mod.rs": { + "./src/rhl/lower.rs": { "content_hash": [ - 23, - 15, - 19, - 160, - 211, - 32, - 74, - 201, - 109, - 242, - 195, - 158, 166, - 208, - 150, - 240, - 15, - 78, - 179, - 152, - 215, + 116, + 206, + 120, + 84, + 21, + 98, + 230, + 31, + 237, + 120, + 12, + 220, + 55, + 90, 243, - 24, - 226, + 50, + 241, + 208, + 110, + 109, + 91, + 53, + 46, + 139, + 230, + 123, + 218, + 244, + 74, + 39, + 225 + ], + "score": { + "structural_complexity": 0.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 80.0, + "grade": "B+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/lower.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 77 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 10.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 88" + }, + { + "source_metric": "StructuralComplexity", + "amount": 15.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 165" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/mod.rs": { + "content_hash": [ + 12, + 40, + 19, 87, - 81, - 129, - 124, - 23, - 165, + 218, + 63, + 156, + 173, + 151, + 60, + 47, + 43, + 119, + 182, + 34, + 237, + 237, + 108, + 186, + 204, + 89, 172, - 255 + 240, + 9, + 238, + 26, + 133, + 45, + 111, + 253, + 229, + 250 ], "score": { "structural_complexity": 25.0, @@ -30516,6 +31014,148 @@ }, "git_context": null }, + "./src/rhl/receipt.rs": { + "content_hash": [ + 120, + 79, + 105, + 100, + 204, + 149, + 56, + 255, + 253, + 141, + 254, + 121, + 104, + 126, + 86, + 180, + 168, + 81, + 209, + 91, + 182, + 60, + 145, + 85, + 225, + 62, + 178, + 34, + 139, + 115, + 188, + 48 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 6.0, + "total": 96.36363, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/receipt.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 4.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 8 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/runtime.rs": { + "content_hash": [ + 213, + 200, + 189, + 210, + 222, + 185, + 111, + 81, + 220, + 193, + 67, + 174, + 236, + 158, + 220, + 0, + 200, + 18, + 195, + 187, + 215, + 66, + 179, + 111, + 158, + 31, + 18, + 170, + 9, + 189, + 121, + 138 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 95.454544, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/runtime.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 11 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/rhl/tree.rs": { "content_hash": [ 0, @@ -30597,38 +31237,38 @@ }, "./src/rhl/vocab.rs": { "content_hash": [ - 82, - 157, - 218, - 111, - 150, - 90, - 240, - 213, - 75, - 108, - 129, - 222, - 235, - 215, - 164, - 239, - 158, - 102, + 30, + 231, + 136, + 64, 176, - 103, - 2, - 192, - 203, - 153, - 188, - 235, - 203, - 133, - 180, + 55, + 65, + 151, + 117, + 198, + 230, + 246, + 236, + 130, + 142, + 201, + 212, + 174, + 17, + 106, + 76, + 204, + 176, + 148, + 173, + 32, + 60, 53, - 44, - 136 + 74, + 130, + 101, + 8 ], "score": { "structural_complexity": 25.0, @@ -30668,38 +31308,38 @@ }, "./src/rhl/vocab_cli.rs": { "content_hash": [ - 150, - 136, - 0, - 80, - 6, - 93, - 194, - 220, - 236, - 229, + 154, + 245, + 32, + 137, + 20, + 12, + 46, + 253, + 203, + 22, + 130, + 213, + 75, + 98, 120, - 218, - 255, - 60, - 41, - 166, - 251, - 176, - 79, + 151, 58, - 234, - 54, - 124, - 164, - 255, - 232, - 102, - 67, - 21, - 104, - 139, - 23 + 82, + 125, + 247, + 90, + 251, + 195, + 108, + 209, + 105, + 140, + 31, + 154, + 180, + 203, + 224 ], "score": { "structural_complexity": 25.0, @@ -30721,7 +31361,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 20 duplicate code patterns" + "issue": "Found 21 duplicate code patterns" } ], "critical_defects_count": 0, @@ -32976,48 +33616,48 @@ }, "./src/runtime/eval_array.rs": { "content_hash": [ - 99, - 216, - 180, + 141, + 7, + 80, + 58, + 143, + 74, + 93, 159, - 241, - 4, - 181, - 167, - 2, - 215, - 163, - 223, - 243, - 2, - 208, + 53, + 141, + 23, + 160, + 180, + 20, + 253, 248, - 13, - 66, - 245, - 177, - 207, - 242, - 175, - 173, + 96, + 105, + 227, + 138, 241, - 156, - 110, - 252, - 106, - 65, - 167, - 117 + 239, + 242, + 242, + 158, + 117, + 61, + 10, + 148, + 114, + 229, + 184 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.015873, + "duplication_ratio": 15.8795185, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.01587, + "total": 75.87952, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33029,15 +33669,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 71 duplicate code patterns" + "issue": "Found 86 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.984127, + "amount": 4.120482, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 24.9%" + "issue": "Code duplication: 20.6%" }, { "source_metric": "StructuralComplexity", @@ -33045,7 +33685,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 100" + "issue": "High cognitive complexity: 104" }, { "source_metric": "StructuralComplexity", @@ -33053,7 +33693,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 131" + "issue": "High cyclomatic complexity: 143" } ], "critical_defects_count": 0, @@ -33071,38 +33711,38 @@ }, "./src/runtime/eval_builtin.rs": { "content_hash": [ - 144, - 156, - 156, - 68, + 173, + 139, + 202, 138, - 196, - 168, - 241, - 181, + 59, + 53, + 219, + 250, + 210, + 152, 134, - 115, - 188, - 72, - 104, - 97, - 236, - 74, - 201, - 234, - 206, - 14, - 252, - 225, + 27, + 248, + 25, + 204, + 11, + 178, + 224, + 255, 24, - 91, - 73, - 19, - 189, - 104, - 44, - 78, - 183 + 4, + 165, + 250, + 115, + 251, + 84, + 149, + 77, + 26, + 213, + 25, + 115 ], "score": { "structural_complexity": 0.0, @@ -35028,47 +35668,47 @@ "./src/runtime/eval_operations.rs": { "content_hash": [ 145, - 0, - 130, - 59, + 124, + 227, + 136, + 252, + 51, + 231, + 54, + 93, + 78, + 231, + 99, + 44, + 218, + 82, + 27, + 147, + 48, + 54, + 189, + 218, + 183, + 57, + 254, 64, + 192, 17, - 240, - 78, - 127, - 180, - 3, - 149, - 135, - 213, - 130, - 194, - 61, + 193, + 184, + 34, 174, - 167, - 104, - 241, - 145, - 146, - 68, - 3, - 37, - 150, - 167, - 227, - 96, - 172, - 117 + 16 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.770863, + "duplication_ratio": 15.779817, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.77086, + "total": 75.779816, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -35084,7 +35724,7 @@ }, { "source_metric": "Duplication", - "amount": 4.2291374, + "amount": 4.2201834, "applied_to": [ "Duplication" ], @@ -35104,7 +35744,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 148" + "issue": "High cyclomatic complexity: 149" } ], "critical_defects_count": 0, @@ -35502,38 +36142,38 @@ }, "./src/runtime/eval_string_methods.rs": { "content_hash": [ - 127, - 5, - 254, - 134, - 19, - 214, - 176, - 86, - 48, - 184, - 115, - 187, - 179, - 171, - 40, - 222, - 19, - 112, - 110, - 120, - 240, - 124, - 249, - 121, - 231, - 91, - 54, - 158, - 198, + 9, + 43, + 96, + 42, + 216, + 156, + 22, + 209, + 157, 94, - 140, - 70 + 10, + 203, + 42, + 82, + 162, + 114, + 157, + 165, + 106, + 204, + 226, + 195, + 117, + 38, + 216, + 66, + 122, + 206, + 82, + 76, + 142, + 180 ], "score": { "structural_complexity": 0.0, @@ -36151,38 +36791,38 @@ }, "./src/runtime/interpreter_control_flow.rs": { "content_hash": [ - 254, - 239, - 119, - 191, - 31, - 73, - 217, - 197, - 249, - 154, - 192, - 5, - 67, - 135, - 238, - 16, - 214, - 102, - 161, + 170, + 25, + 175, + 106, + 52, + 210, + 0, + 147, + 159, + 151, + 233, + 22, + 211, + 163, + 242, 127, - 23, + 192, + 198, + 247, + 108, + 13, + 199, 96, - 184, - 204, - 139, - 42, - 117, - 164, - 69, - 28, - 82, - 202 + 130, + 147, + 51, + 229, + 114, + 47, + 205, + 12, + 94 ], "score": { "structural_complexity": 0.0, @@ -36349,48 +36989,48 @@ }, "./src/runtime/interpreter_functions.rs": { "content_hash": [ - 119, - 76, - 12, - 224, - 8, - 143, - 17, - 197, - 126, - 20, - 255, - 29, - 209, + 82, + 198, + 254, + 164, + 239, + 157, + 93, + 221, + 146, + 7, + 102, + 248, + 7, + 166, + 232, + 103, + 155, + 246, + 141, + 100, 208, - 25, - 229, - 187, - 173, - 37, - 156, - 194, - 176, - 241, - 46, - 194, - 253, - 95, - 119, - 49, - 95, - 135, - 72 + 129, + 216, + 179, + 203, + 132, + 215, + 143, + 81, + 60, + 203, + 30 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.461182, + "duplication_ratio": 14.469526, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 74.46118, + "total": 74.46953, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -36410,11 +37050,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 98 duplicate code patterns" + "issue": "Found 103 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.5388184, + "amount": 5.530474, "applied_to": [ "Duplication" ], @@ -36426,15 +37066,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 114" + "issue": "High cognitive complexity: 116" }, { "source_metric": "StructuralComplexity", - "amount": 13.0, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 56" + "issue": "High cyclomatic complexity: 64" } ], "critical_defects_count": 0, @@ -36625,48 +37265,48 @@ }, "./src/runtime/interpreter_methods_actor.rs": { "content_hash": [ - 104, - 239, - 153, - 135, - 143, - 185, - 32, - 184, - 218, + 103, + 79, + 111, 13, - 140, - 71, - 199, - 17, - 237, - 162, - 14, - 239, + 208, + 118, + 230, + 89, + 25, + 249, + 234, + 178, + 185, + 58, + 143, 52, - 17, - 144, - 80, - 80, - 152, - 122, - 15, - 42, + 221, + 151, + 176, + 134, + 116, + 43, + 138, + 73, + 208, 188, - 252, + 224, + 7, + 117, + 147, 103, - 241, - 8 + 143 ], "score": { "structural_complexity": 9.5, "semantic_complexity": 20.0, - "duplication_ratio": 17.407408, + "duplication_ratio": 17.421053, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 86.90741, + "total": 86.92105, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -36682,11 +37322,11 @@ }, { "source_metric": "Duplication", - "amount": 2.5925925, + "amount": 2.5789473, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.0%" + "issue": "Code duplication: 12.9%" }, { "source_metric": "StructuralComplexity", @@ -36720,48 +37360,48 @@ }, "./src/runtime/interpreter_methods_dispatch.rs": { "content_hash": [ - 33, - 121, - 164, - 105, - 218, - 76, - 252, - 192, + 226, 108, - 57, - 234, - 53, - 119, - 244, - 4, - 56, - 6, - 219, - 224, - 102, - 217, - 105, - 255, - 194, - 61, - 16, - 205, + 246, + 128, + 127, + 41, + 175, + 235, + 212, + 162, + 113, + 49, + 140, + 106, + 44, + 215, + 129, + 76, 232, - 28, - 19, - 79, - 102 + 71, + 65, + 70, + 138, + 121, + 114, + 184, + 241, + 18, + 172, + 172, + 88, + 165 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 13.974066, + "duplication_ratio": 14.124374, "coupling_score": 11.4, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 70.37407, + "total": 70.524376, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -36781,15 +37421,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 175 duplicate code patterns" + "issue": "Found 179 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.025934, + "amount": 5.8756256, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 30.1%" + "issue": "Code duplication: 29.4%" }, { "source_metric": "StructuralComplexity", @@ -36797,7 +37437,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 122" + "issue": "High cognitive complexity: 117" }, { "source_metric": "StructuralComplexity", @@ -36805,7 +37445,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 68" + "issue": "High cyclomatic complexity: 70" }, { "source_metric": "Coupling", @@ -37456,48 +38096,48 @@ }, "./src/runtime/interpreter_types_class.rs": { "content_hash": [ - 224, - 22, + 150, + 140, + 146, + 6, + 5, + 240, 215, + 14, + 96, + 141, + 141, + 179, 161, - 40, - 155, - 41, - 177, - 103, - 249, - 171, - 145, - 0, - 139, - 235, - 254, - 43, - 17, - 68, - 56, - 222, - 229, - 15, - 137, - 246, - 216, - 178, - 46, - 149, - 112, - 236, - 127 + 214, + 197, + 108, + 247, + 244, + 52, + 65, + 247, + 105, + 241, + 37, + 232, + 111, + 7, + 189, + 98, + 132, + 20, + 107 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.80498, + "duplication_ratio": 16.882591, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.80498, + "total": 76.88259, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -37517,15 +38157,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 62 duplicate code patterns" + "issue": "Found 61 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.195021, + "amount": 3.117409, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 16.0%" + "issue": "Code duplication: 15.6%" }, { "source_metric": "StructuralComplexity", @@ -37533,7 +38173,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 181" + "issue": "High cognitive complexity: 177" }, { "source_metric": "StructuralComplexity", @@ -38816,48 +39456,48 @@ }, "./src/runtime/repl/evaluation.rs": { "content_hash": [ + 44, + 74, + 136, + 253, 83, - 32, - 118, - 98, - 37, - 80, - 57, - 87, - 140, - 224, - 160, - 238, - 102, - 94, - 205, - 165, - 146, - 197, - 68, - 225, - 241, - 205, + 233, + 166, + 23, 138, - 89, 77, - 34, - 205, - 102, - 30, - 33, - 157, - 136 + 131, + 140, + 240, + 11, + 241, + 222, + 173, + 3, + 179, + 82, + 202, + 80, + 142, + 179, + 245, + 227, + 56, + 47, + 210, + 70, + 199, + 85 ], "score": { - "structural_complexity": 1.2999992, + "structural_complexity": 0.39999962, "semantic_complexity": 20.0, - "duplication_ratio": 16.987951, + "duplication_ratio": 17.1537, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 78.28795, + "total": 77.553696, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -38873,19 +39513,19 @@ }, { "source_metric": "Duplication", - "amount": 3.0120482, + "amount": 2.8462996, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.1%" + "issue": "Code duplication: 14.2%" }, { "source_metric": "StructuralComplexity", - "amount": 8.700001, + "amount": 9.6, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 44" + "issue": "High cognitive complexity: 47" }, { "source_metric": "StructuralComplexity", @@ -38893,7 +39533,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 75" + "issue": "High cyclomatic complexity: 79" } ], "critical_defects_count": 0, @@ -39960,49 +40600,49 @@ }, "./src/runtime/value.rs": { "content_hash": [ - 230, - 96, - 88, - 3, + 43, + 11, 91, - 102, - 214, - 29, - 178, - 151, - 92, - 212, - 8, - 159, - 96, - 109, - 136, - 125, - 127, - 57, - 219, - 108, - 159, - 22, - 177, - 71, + 215, + 99, + 13, + 95, + 51, + 143, + 117, + 200, + 53, + 105, + 10, + 141, + 46, + 152, + 228, 91, - 248, - 42, - 205, - 250, - 210 + 40, + 126, + 185, + 220, + 244, + 188, + 126, + 162, + 208, + 93, + 237, + 226, + 176 ], "score": { - "structural_complexity": 21.5, + "structural_complexity": 19.5, "semantic_complexity": 20.0, "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.27273, - "grade": "A", + "total": 99.5, + "grade": "A+", "confidence": 1.0, "language": "Rust", "file_path": "./src/runtime/value.rs", @@ -40013,15 +40653,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 12 duplicate code patterns" + "issue": "Found 15 duplicate code patterns" }, { "source_metric": "StructuralComplexity", - "amount": 3.5, + "amount": 5.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 37" + "issue": "High cyclomatic complexity: 41" } ], "critical_defects_count": 0, @@ -40300,48 +40940,48 @@ }, "./src/server/watcher.rs": { "content_hash": [ - 128, - 138, - 162, - 107, - 156, - 92, - 30, - 145, - 190, - 93, - 114, - 66, + 135, + 46, + 184, + 173, + 85, + 74, + 246, + 78, + 55, + 160, + 176, + 191, 16, - 112, - 1, - 151, - 174, - 192, - 162, - 105, - 169, - 121, - 231, - 105, - 44, - 157, - 151, - 43, - 187, - 198, - 24, - 114 + 229, + 159, + 218, + 51, + 104, + 219, + 178, + 63, + 160, + 156, + 219, + 116, + 200, + 152, + 186, + 205, + 177, + 60, + 187 ], "score": { - "structural_complexity": 25.0, + "structural_complexity": 24.7, "semantic_complexity": 20.0, - "duplication_ratio": 16.556292, + "duplication_ratio": 17.41573, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.3239, + "total": 92.83248, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -40357,11 +40997,19 @@ }, { "source_metric": "Duplication", - "amount": 3.4437087, + "amount": 2.5842695, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 17.2%" + "issue": "Code duplication: 12.9%" + }, + { + "source_metric": "StructuralComplexity", + "amount": 0.3, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 16" } ], "critical_defects_count": 0, @@ -41777,38 +42425,38 @@ }, "./src/stdlib/trueno_bridge.rs": { "content_hash": [ - 208, - 228, - 201, - 153, - 221, - 3, - 80, - 80, - 80, - 196, - 29, - 242, - 56, + 180, + 174, + 192, + 124, + 172, + 94, + 141, + 120, + 8, + 132, + 61, + 69, + 97, + 94, + 180, + 86, + 216, + 42, + 200, + 207, + 247, + 224, 5, + 123, 223, - 13, - 158, - 239, - 62, - 175, - 108, - 238, - 75, - 34, - 70, - 240, - 146, - 152, - 113, - 70, - 75, - 31 + 15, + 78, + 171, + 176, + 9, + 221, + 44 ], "score": { "structural_complexity": 25.0, @@ -44144,13 +44792,13 @@ } }, "summary": { - "total_files": 519, - "avg_score": 90.31551, + "total_files": 527, + "avg_score": 90.318634, "grade_distribution": { - "A+": 189, + "A+": 194, "A": 176, - "A-": 30, - "B+": 28, + "A-": 31, + "B+": 30, "B": 67, "B-": 26, "C": 1, @@ -44159,7 +44807,7 @@ "languages": { "JavaScript": 14, "Python": 4, - "Rust": 497, + "Rust": 505, "TypeScript": 4 } } diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index b841f6dba..591d4ac2a 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -36,6 +36,39 @@ impl Transpiler { self.transpile_regular_function_call(&func_tokens, args) } + /// PARSER-094: Render a callee `a::b::c` (a FieldAccess chain rooted at a + /// lowercase module identifier) as a Rust path. `self.f` is a field, not a path. + fn module_call_path(func: &Expr) -> Option { + let segments = Self::callee_path_segments(func)?; + let root = segments.first()?; + let is_module_root = + root.starts_with(|c: char| c.is_ascii_lowercase()) && root != "self" && root != "std"; + if segments.len() < 2 || !is_module_root { + return None; + } + let idents = segments.iter().map(|s| format_ident!("{}", s)); + Some(quote! { #(#idents)::* }) + } + + /// Collect the identifier segments of a FieldAccess chain; None if any + /// segment is not a plain identifier (tuple index, call, literal, ...). + fn callee_path_segments(expr: &Expr) -> Option> { + match &expr.kind { + ExprKind::Identifier(name) if Self::is_plain_ident(name) => Some(vec![name.clone()]), + ExprKind::FieldAccess { object, field } if Self::is_plain_ident(field) => { + let mut segments = Self::callee_path_segments(object)?; + segments.push(field.clone()); + Some(segments) + } + _ => None, + } + } + + fn is_plain_ident(name: &str) -> bool { + name.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') + && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + } + /// Transform main() calls to __ruchy_main() fn transform_main_call(&self, func: &Expr) -> Result { if let ExprKind::Identifier(name) = &func.kind { @@ -44,6 +77,11 @@ impl Transpiler { return Ok(quote! { #renamed_ident }); } } + // PARSER-094 (Issue #137): `module::function(..)` parses as Call{FieldAccess} + // (`obj.method(..)` is a MethodCall), so a callee path keeps `::`. + if let Some(path_tokens) = Self::module_call_path(func) { + return Ok(path_tokens); + } self.transpile_expr(func) } From d0bd9ab84aac156b3171a8c75fecdea6adaf8bd0 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:39:33 +0200 Subject: [PATCH 003/106] G2B-T2: a bare self receiver stays owned in transpiled impls (QUALITY-001, SPEC 7.11) self_receiver_tokens turned every bare `self` into `&self` (TRANSPILER-METHOD-SELF-001), contradicting SPECIFICATION 7.11 (`fun transfer(self)` moves). Now: &self/&mut self kept, `mut self` kept, bare self -> self, and a bare self whose body mutates it -> &mut self (an immutable owned receiver could not compile). Fixes 4 tests in tests/quality_001_self_receiver.rs; backend::transpiler lib tests 3233/3233. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 76 ++++++++++++++++----------------- src/backend/transpiler/types.rs | 29 ++++++------- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index eb5005729..4db35ab05 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:05.178092247Z", + "created_at": "2026-09-24T00:02:07.900273165Z", "git_context": null, "files": { "./build.rs": { @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 197, + 214, + 245, + 174, 159, - 185, - 63, - 204, - 21, - 95, - 14, - 229, - 178, - 11, - 42, - 232, + 41, + 253, + 217, + 216, + 216, + 67, + 176, + 31, 65, - 23, - 130, - 204, - 52, - 101, - 12, - 81, - 204, - 147, - 10, - 22, - 167, - 110, - 7, + 9, 7, - 219, - 109, - 146 + 124, + 36, + 215, + 108, + 93, + 37, + 106, + 229, + 23, + 158, + 25, + 146, + 242, + 104, + 196, + 71, + 194 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.546772, + "duplication_ratio": 15.71066, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.54677, + "total": 75.71066, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3643,11 +3643,11 @@ }, { "source_metric": "Duplication", - "amount": 4.453228, + "amount": 4.28934, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 22.3%" + "issue": "Code duplication: 21.4%" }, { "source_metric": "StructuralComplexity", @@ -3655,15 +3655,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 64" + "issue": "High cognitive complexity: 67" }, { "source_metric": "StructuralComplexity", - "amount": 14.0, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 58" + "issue": "High cyclomatic complexity: 63" } ], "critical_defects_count": 0, @@ -44793,7 +44793,7 @@ }, "summary": { "total_files": 527, - "avg_score": 90.318634, + "avg_score": 90.31895, "grade_distribution": { "A+": 194, "A": 176, diff --git a/src/backend/transpiler/types.rs b/src/backend/transpiler/types.rs index 878353e7e..312316af1 100644 --- a/src/backend/transpiler/types.rs +++ b/src/backend/transpiler/types.rs @@ -12,20 +12,19 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::Lifetime; -/// Generate self-receiver tokens based on type kind and mutation flag. +/// Generate self-receiver tokens for a `self` parameter. /// -/// Reduces nesting in match arms that emit `quote! { &mut self }` or `quote! { &self }`. -fn self_receiver_tokens(ty: &TypeKind, mutated_fallback: bool) -> TokenStream { - if let TypeKind::Reference { is_mut, .. } = ty { - if *is_mut { - quote! { &mut self } - } else { - quote! { &self } - } - } else if mutated_fallback { - quote! { &mut self } - } else { - quote! { &self } +/// `&self` / `&mut self` are kept. A bare `self` is an owned receiver (SPECIFICATION +/// §7.11, QUALITY-001): `self`, or `mut self` when declared so. A bare `self` whose +/// body mutates it cannot compile as an immutable owned receiver, so it becomes +/// `&mut self` (TRANSPILER-METHOD-SELF-001). +fn self_receiver_tokens(param: &crate::frontend::ast::Param, mutated: bool) -> TokenStream { + match ¶m.ty.kind { + TypeKind::Reference { is_mut: true, .. } => quote! { &mut self }, + TypeKind::Reference { .. } => quote! { &self }, + _ if param.is_mutable => quote! { mut self }, + _ if mutated => quote! { &mut self }, + _ => quote! { self }, } } @@ -1087,7 +1086,7 @@ impl Transpiler { // Legacy: name-based detection (e.g., "&self" as name) quote! { &self } } else { - self_receiver_tokens(¶m.ty.kind, self_is_mutated) + self_receiver_tokens(param, self_is_mutated) } } else { let param_name = format_ident!("{}", param.name()); @@ -1183,7 +1182,7 @@ impl Transpiler { // Method receivers in Rust have special syntax that differs from normal parameters if name == "self" { // QUALITY-001 + TRANSPILER-METHOD-SELF-001: self receiver - self_receiver_tokens(¶m.ty.kind, self_is_mutated) + self_receiver_tokens(param, self_is_mutated) } else { let param_name = format_ident!("{}", param.name()); let type_tokens = self From 651681799514a9c2ebe065ba3dcb4473f9bc33e8 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:42:11 +0200 Subject: [PATCH 004/106] G2B-T3: a fn returning a let-bound [] infers -> Vec (ISSUE-113) returns_vec only saw a trailing list literal; `let mut arr = []; arr.push(..); arr` fell through to the numeric default -> i32. The Block tail check (block_tail_is_vec) now also traces a trailing identifier to its let-bound list. Fixes 2 tests in tests/issue_113_transpiler_type_inference.rs; the transpiled make_array compiles with rustc. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 80 +++++++++---------- src/backend/transpiler/return_type_helpers.rs | 13 +-- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 4db35ab05..f70acae5f 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:07.900273165Z", + "created_at": "2026-09-24T00:02:09.655347485Z", "git_context": null, "files": { "./build.rs": { @@ -8802,48 +8802,48 @@ }, "./src/backend/transpiler/types.rs": { "content_hash": [ - 217, - 10, - 232, - 214, - 75, - 78, - 9, - 111, - 254, - 146, - 105, + 43, + 6, + 237, 170, - 154, - 239, - 103, - 186, - 48, - 30, - 232, - 143, - 68, - 66, - 127, - 159, - 118, - 243, - 94, - 140, - 8, - 104, - 153, - 248 + 101, + 218, + 2, + 58, + 102, + 76, + 212, + 136, + 54, + 191, + 216, + 77, + 136, + 52, + 209, + 254, + 210, + 56, + 1, + 77, + 73, + 0, + 72, + 58, + 67, + 28, + 238, + 33 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.392405, + "duplication_ratio": 15.461473, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.3924, + "total": 75.46147, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -8863,15 +8863,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 137 duplicate code patterns" + "issue": "Found 135 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.607595, + "amount": 4.5385265, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 23.0%" + "issue": "Code duplication: 22.7%" }, { "source_metric": "StructuralComplexity", @@ -8879,7 +8879,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 157" + "issue": "High cognitive complexity: 153" }, { "source_metric": "StructuralComplexity", @@ -8887,7 +8887,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 130" + "issue": "High cyclomatic complexity: 132" } ], "critical_defects_count": 0, @@ -44793,7 +44793,7 @@ }, "summary": { "total_files": 527, - "avg_score": 90.31895, + "avg_score": 90.31908, "grade_distribution": { "A+": 194, "A": 176, diff --git a/src/backend/transpiler/return_type_helpers.rs b/src/backend/transpiler/return_type_helpers.rs index f8877a60f..78f3065c7 100644 --- a/src/backend/transpiler/return_type_helpers.rs +++ b/src/backend/transpiler/return_type_helpers.rs @@ -157,12 +157,7 @@ pub fn returns_vec(body: &Expr) -> bool { ExprKind::Return { value: Some(val) } => returns_vec(val), // Block - check last expression - ExprKind::Block(exprs) => exprs.last().is_some_and(|e| { - // Last expression is array literal - matches!(&e.kind, ExprKind::List(_)) - // OR recursively check - || returns_vec(e) - }), + ExprKind::Block(exprs) => exprs.last().is_some_and(|e| block_tail_is_vec(e, exprs)), // Let expression - check body ExprKind::Let { body: let_body, .. } | ExprKind::LetPattern { body: let_body, .. } => { @@ -173,6 +168,12 @@ pub fn returns_vec(body: &Expr) -> bool { } } +/// Is the tail `e` of a block `exprs` a Vec: a list literal, a nested Vec-returning +/// expression, or (ISSUE-113) an identifier bound to a list earlier in the block. +fn block_tail_is_vec(e: &Expr, exprs: &[Expr]) -> bool { + matches!(&e.kind, ExprKind::List(_)) || returns_vec(e) || expr_creates_or_returns_vec(e, exprs) +} + /// TRANSPILER-013: Check if expression returns an object literal (transpiled to `BTreeMap`) /// Used to infer return type annotation for functions pub fn returns_object_literal(body: &Expr) -> bool { From 5f773c276db481651f5e930b14966e717821b708 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:44:17 +0200 Subject: [PATCH 005/106] G2B-T4: @test("description") transpiles to #[doc] #[test], not #[test(description)] (BUG-033) format_regular_attribute_impl copied attribute args verbatim; Rust's #[test] takes none. The description is kept as a doc comment. Fixes 4 tests in tests/bug_033_test_attribute_invalid_rust.rs (compile + property-tests paths now build). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 58 ++++++++++---------- src/backend/transpiler/function_signature.rs | 13 +++++ 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index f70acae5f..c877139c7 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:09.655347485Z", + "created_at": "2026-09-24T00:02:11.347132547Z", "git_context": null, "files": { "./build.rs": { @@ -7924,38 +7924,38 @@ }, "./src/backend/transpiler/return_type_helpers.rs": { "content_hash": [ - 145, - 175, - 214, - 40, + 255, + 92, + 236, + 189, + 185, + 193, + 156, 143, - 55, - 61, - 180, - 47, - 69, - 184, - 24, - 53, - 214, - 244, - 88, - 58, - 132, - 119, - 237, - 51, - 26, 118, - 27, + 110, + 58, + 206, + 109, + 200, + 104, + 20, + 207, + 240, + 60, + 93, 8, - 45, + 237, + 38, + 54, 109, - 29, - 40, - 107, - 185, - 31 + 188, + 63, + 128, + 9, + 197, + 101, + 119 ], "score": { "structural_complexity": 0.0, diff --git a/src/backend/transpiler/function_signature.rs b/src/backend/transpiler/function_signature.rs index 339782248..f9a7c1960 100644 --- a/src/backend/transpiler/function_signature.rs +++ b/src/backend/transpiler/function_signature.rs @@ -323,6 +323,9 @@ impl Transpiler { /// Complexity: 4 (within Toyota Way limits) pub(crate) fn format_regular_attribute_impl(&self, attr: &Attribute) -> TokenStream { let attr_name = format_ident!("{}", attr.name); + if attr.name == "test" { + return Self::format_test_attribute(&attr.args); + } if attr.args.is_empty() { quote! { #[#attr_name] } } else { @@ -335,6 +338,16 @@ impl Transpiler { } } + /// BUG-033: Rust's `#[test]` takes no arguments; `@test("description")` + /// keeps its description as a doc comment on the test function. + fn format_test_attribute(args: &[String]) -> TokenStream { + if args.is_empty() { + return quote! { #[test] }; + } + let doc = args.join(", ").trim_matches('"').to_string(); + quote! { #[doc = #doc] #[test] } + } + /// Compute final return type (handles special cases) /// Complexity: 1 (within Toyota Way limits) pub(crate) fn compute_final_return_type_impl( From 1fead4e739c5d3995d3717264a6dc0338cba1982 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:52:06 +0200 Subject: [PATCH 006/106] G2B-S2: ruchy serve tests gated on the notebook feature; readiness polling - root cause: default features (batteries-included) do not include notebook, so handle_serve_command is the stub; server-spawning tests could never connect - server tests cfg(feature = "notebook"); new default-build test asserts the stub names --features notebook and prints the ENVIRONMENT reason - serve validates the directory in every build (validate_serve_directory) - fixed 500 ms sleeps replaced by wait_for_port() TCP readiness polling - with --features notebook: http_server_cli 13/13 pass; http_watch_mode test_serve_with_watch_starts_successfully still fails (server exits under --watch) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 84 +++++++++++++++---------------- src/bin/handlers/serve_handler.rs | 39 ++++++++------ tests/http_server_cli.rs | 64 +++++++++++++++++++---- tests/http_watch_mode.rs | 12 ++--- 4 files changed, 125 insertions(+), 74 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index c877139c7..c463a7a1d 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:11.347132547Z", + "created_at": "2026-09-24T00:02:13.146427939Z", "git_context": null, "files": { "./build.rs": { @@ -6091,48 +6091,48 @@ }, "./src/backend/transpiler/function_signature.rs": { "content_hash": [ - 216, - 12, - 223, - 11, + 217, + 87, + 242, + 178, + 187, + 154, + 105, + 134, + 100, + 39, + 8, + 245, + 119, + 59, + 71, + 105, + 179, + 87, + 167, + 158, + 186, + 189, + 85, + 231, + 237, + 227, + 87, + 74, 159, - 233, - 88, - 246, - 253, - 215, - 33, - 95, - 152, - 73, - 218, - 116, - 72, - 94, - 5, - 169, - 79, - 75, - 147, - 46, - 48, - 174, - 222, - 141, - 19, - 137, - 160, - 213 + 200, + 165, + 140 ], "score": { - "structural_complexity": 0.09999943, + "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.774971, + "duplication_ratio": 16.813318, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.87497, + "total": 76.81332, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -6156,27 +6156,27 @@ }, { "source_metric": "Duplication", - "amount": 3.22503, + "amount": 3.1866825, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 16.1%" + "issue": "Code duplication: 15.9%" }, { "source_metric": "StructuralComplexity", - "amount": 9.900001, + "amount": 10.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 48" + "issue": "High cognitive complexity: 50" }, { "source_metric": "StructuralComplexity", - "amount": 14.0, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 58" + "issue": "High cyclomatic complexity: 60" } ], "critical_defects_count": 0, @@ -44793,7 +44793,7 @@ }, "summary": { "total_files": 527, - "avg_score": 90.31908, + "avg_score": 90.31896, "grade_distribution": { "A+": 194, "A": 176, diff --git a/src/bin/handlers/serve_handler.rs b/src/bin/handlers/serve_handler.rs index 087b5be9e..edd8202d8 100644 --- a/src/bin/handlers/serve_handler.rs +++ b/src/bin/handlers/serve_handler.rs @@ -5,6 +5,26 @@ use anyhow::Result; use std::path::Path; +/// Reject a serve root that is missing or is not a directory. +/// +/// Runs in every build so `ruchy serve ./missing` reports the path problem +/// whether or not the HTTP server itself was compiled in. +fn validate_serve_directory(directory: &Path) -> Result<()> { + if !directory.exists() { + return Err(anyhow::anyhow!( + "Directory not found: {}", + directory.display() + )); + } + if !directory.is_dir() { + return Err(anyhow::anyhow!( + "Path is not a directory: {}", + directory.display() + )); + } + Ok(()) +} + /// Handle serve command - serve static files over HTTP /// /// # Arguments @@ -31,19 +51,7 @@ pub fn handle_serve_command( use tower::ServiceBuilder; use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer}; - // Verify directory exists - if !directory.exists() { - return Err(anyhow::anyhow!( - "Directory not found: {}", - directory.display() - )); - } - if !directory.is_dir() { - return Err(anyhow::anyhow!( - "Path is not a directory: {}", - directory.display() - )); - } + validate_serve_directory(directory)?; // Initialize PID file if requested let _pid_guard = if let Some(pid_path) = pid_file { @@ -384,7 +392,7 @@ fn run_normal_mode( #[cfg(not(feature = "notebook"))] pub fn handle_serve_command( - _directory: &Path, + directory: &Path, _port: u16, _host: &str, _verbose: bool, @@ -393,6 +401,7 @@ pub fn handle_serve_command( _pid_file: Option<&Path>, _watch_wasm: bool, ) -> Result<()> { + validate_serve_directory(directory)?; Err(anyhow::anyhow!( "HTTP server requires notebook feature. Rebuild with --features notebook" )) @@ -412,7 +421,6 @@ mod tests { // ===== EXTREME TDD Round 145 - Serve Handler Tests ===== #[test] - #[cfg(feature = "notebook")] fn test_handle_serve_command_nonexistent_dir() { let result = handle_serve_command( Path::new("/nonexistent/dir"), @@ -429,7 +437,6 @@ mod tests { } #[test] - #[cfg(feature = "notebook")] fn test_handle_serve_command_file_not_dir() { let temp_dir = TempDir::new().unwrap(); let file_path = temp_dir.path().join("test.txt"); diff --git a/tests/http_server_cli.rs b/tests/http_server_cli.rs index 8f5e8bed3..05bca20fa 100644 --- a/tests/http_server_cli.rs +++ b/tests/http_server_cli.rs @@ -26,6 +26,22 @@ fn find_available_port() -> u16 { .port() } +/// Helper: Block until the server accepts TCP connections on `port`. +/// +/// Polls for readiness instead of sleeping a fixed interval, so a slow debug +/// build under parallel load does not race the first request. +#[cfg(feature = "notebook")] +fn wait_for_port(port: u16) { + let deadline = std::time::Instant::now() + Duration::from_secs(15); + while std::time::Instant::now() < deadline { + if std::net::TcpStream::connect(("127.0.0.1", port)).is_ok() { + return; + } + std::thread::sleep(Duration::from_millis(50)); + } + panic!("ruchy serve did not accept connections on port {port} within 15s"); +} + /// Helper: Create test directory with files fn create_test_dir() -> TempDir { let dir = TempDir::new().expect("Failed to create temp dir"); @@ -73,6 +89,7 @@ fn test_red_ruchy_serve_requires_directory() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_red_ruchy_serve_starts_server() { // RED: This MUST fail - ruchy serve doesn't exist yet use std::process::{Command, Stdio}; @@ -94,8 +111,7 @@ fn test_red_ruchy_serve_starts_server() { .spawn() .expect("Failed to spawn server"); - // Give server time to start - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); // Test HTTP request let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/index.html")); @@ -109,6 +125,25 @@ fn test_red_ruchy_serve_starts_server() { child.kill().expect("Failed to kill server"); } +#[test] +#[cfg(not(feature = "notebook"))] +fn test_serve_without_notebook_feature_names_the_rebuild_flag() { + // The server tests in this file need a binary built with `--features notebook`; + // default builds must say so instead of failing opaquely. + println!( + "ENVIRONMENT: ruchy built without the notebook feature; server tests are compiled out" + ); + let test_dir = create_test_dir(); + ruchy_cmd() + .arg("serve") + .arg(test_dir.path()) + .arg("--port") + .arg(find_available_port().to_string()) + .assert() + .failure() + .stderr(predicate::str::contains("--features notebook")); +} + #[test] #[ignore = "RED phase: ruchy serve command not yet implemented"] fn test_red_ruchy_serve_default_port_8080() { @@ -141,6 +176,7 @@ fn test_red_ruchy_serve_default_port_8080() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_red_ruchy_serve_shows_startup_message() { // RED: This MUST fail - ruchy serve doesn't exist yet let test_dir = create_test_dir(); @@ -220,6 +256,7 @@ mod property_tests { // ============================================================================ #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_http002_mime_html() { let test_dir = TempDir::new().unwrap(); std::fs::write( @@ -240,7 +277,7 @@ fn test_http002_mime_html() { .spawn() .expect("Failed to spawn server"); - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/index.html")).unwrap(); assert_eq!(response.status(), 200); @@ -250,6 +287,7 @@ fn test_http002_mime_html() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_http002_mime_css() { let test_dir = TempDir::new().unwrap(); std::fs::write(test_dir.path().join("style.css"), "body { margin: 0; }").unwrap(); @@ -266,7 +304,7 @@ fn test_http002_mime_css() { .spawn() .expect("Failed to spawn server"); - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/style.css")).unwrap(); assert_eq!(response.status(), 200); @@ -276,6 +314,7 @@ fn test_http002_mime_css() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_http002_mime_javascript() { let test_dir = TempDir::new().unwrap(); std::fs::write(test_dir.path().join("app.js"), "console.log('test');").unwrap(); @@ -292,7 +331,7 @@ fn test_http002_mime_javascript() { .spawn() .expect("Failed to spawn server"); - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/app.js")).unwrap(); assert_eq!(response.status(), 200); @@ -311,6 +350,7 @@ fn test_http002_mime_javascript() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_http002_mime_wasm() { // CRITICAL: WASM files MUST have application/wasm MIME type let test_dir = TempDir::new().unwrap(); @@ -333,7 +373,7 @@ fn test_http002_mime_wasm() { .spawn() .expect("Failed to spawn server"); - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/module.wasm")).unwrap(); assert_eq!(response.status(), 200); @@ -347,6 +387,7 @@ fn test_http002_mime_wasm() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_http002_mime_json() { let test_dir = TempDir::new().unwrap(); std::fs::write(test_dir.path().join("data.json"), r#"{"test": true}"#).unwrap(); @@ -363,7 +404,7 @@ fn test_http002_mime_json() { .spawn() .expect("Failed to spawn server"); - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/data.json")).unwrap(); assert_eq!(response.status(), 200); @@ -380,6 +421,7 @@ fn test_http002_mime_json() { // ============================================================================ #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_http003_wasm_coop_header() { // CRITICAL: WASM files MUST have Cross-Origin-Opener-Policy for SharedArrayBuffer let test_dir = TempDir::new().unwrap(); @@ -401,7 +443,7 @@ fn test_http003_wasm_coop_header() { .spawn() .expect("Failed to spawn server"); - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/app.wasm")).unwrap(); assert_eq!(response.status(), 200); @@ -418,6 +460,7 @@ fn test_http003_wasm_coop_header() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_http003_wasm_coep_header() { // CRITICAL: WASM files MUST have Cross-Origin-Embedder-Policy for SharedArrayBuffer let test_dir = TempDir::new().unwrap(); @@ -439,7 +482,7 @@ fn test_http003_wasm_coep_header() { .spawn() .expect("Failed to spawn server"); - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/app.wasm")).unwrap(); assert_eq!(response.status(), 200); @@ -456,6 +499,7 @@ fn test_http003_wasm_coep_header() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_http003_html_coop_header() { // HTML files serving WASM also need COOP/COEP headers let test_dir = TempDir::new().unwrap(); @@ -477,7 +521,7 @@ fn test_http003_html_coop_header() { .spawn() .expect("Failed to spawn server"); - std::thread::sleep(Duration::from_millis(500)); + wait_for_port(port); let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/index.html")).unwrap(); assert_eq!(response.status(), 200); diff --git a/tests/http_watch_mode.rs b/tests/http_watch_mode.rs index 1bfef7e93..9c93b9c4b 100644 --- a/tests/http_watch_mode.rs +++ b/tests/http_watch_mode.rs @@ -10,7 +10,6 @@ use assert_cmd::Command; use std::fs; -use std::thread; use std::time::Duration; use tempfile::TempDir; @@ -36,6 +35,7 @@ fn test_serve_help_shows_watch_flags() { } #[test] +#[cfg(feature = "notebook")] // `ruchy serve` is compiled only with the notebook feature fn test_serve_with_watch_starts_successfully() { let temp_dir = TempDir::new().unwrap(); let test_file = temp_dir.path().join("index.html"); @@ -57,7 +57,7 @@ fn test_serve_with_watch_starts_successfully() { .expect("Failed to start server"); // Give server time to start - thread::sleep(Duration::from_secs(2)); + std::thread::sleep(Duration::from_secs(2)); // Server should be running assert!( @@ -117,7 +117,7 @@ fn test_pid_file_replaces_stale_entry() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, feature = "notebook"))] // `ruchy serve` needs the notebook feature fn test_graceful_shutdown_on_sigterm() { use nix::sys::signal::{kill, Signal}; use nix::unistd::Pid; @@ -143,7 +143,7 @@ fn test_graceful_shutdown_on_sigterm() { .expect("Failed to start server"); // Give server time to start and create PID file - thread::sleep(Duration::from_secs(2)); + std::thread::sleep(Duration::from_secs(2)); // PID file should exist assert!(pid_path.exists(), "PID file should be created"); @@ -159,7 +159,7 @@ fn test_graceful_shutdown_on_sigterm() { if child.try_wait().unwrap().is_some() { break; } - thread::sleep(Duration::from_millis(100)); + std::thread::sleep(Duration::from_millis(100)); } // Server should have exited gracefully @@ -170,7 +170,7 @@ fn test_graceful_shutdown_on_sigterm() { ); // PID file should be cleaned up - thread::sleep(Duration::from_millis(100)); // Give time for cleanup + std::thread::sleep(Duration::from_millis(100)); // Give time for cleanup assert!( !pid_path.exists(), "PID file should be removed on graceful shutdown" From 90dcc5ef979a5f95ee179e01560b629beeb37c11 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:52:12 +0200 Subject: [PATCH 007/106] G2B-S3: compute_hash returns MD5 again (STDLIB-005 contract), in-tree RFC 1321 - root cause: 73569f24 (dependency-count chore) swapped the md5 crate for sha2, silently changing compute_hash from 32-hex MD5 to 64-hex SHA-256; docstring, STDLIB-005 tests and ticket all specify MD5 - src/stdlib/hash.rs: md5_hex/md5_digest, RFC 1321 A.5 suite + padding boundaries + proptest (32 lower-hex); no new crate dependency - METHODS-1 lib tests updated from SHA-256 to MD5 digests Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 70 +++--- src/runtime/eval_builtin_fs.rs | 5 +- .../tests/eval_builtin_tests_part1.rs | 7 +- .../tests/eval_builtin_tests_part2.rs | 12 +- src/stdlib/hash.rs | 212 ++++++++++++++++++ src/stdlib/mod.rs | 1 + 6 files changed, 254 insertions(+), 53 deletions(-) create mode 100644 src/stdlib/hash.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index c463a7a1d..5106ef811 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:13.146427939Z", + "created_at": "2026-09-24T00:02:14.898957255Z", "git_context": null, "files": { "./build.rs": { @@ -13055,48 +13055,48 @@ }, "./src/bin/handlers/serve_handler.rs": { "content_hash": [ - 59, - 166, - 176, + 122, + 31, + 120, + 31, + 188, + 196, + 242, + 198, 38, - 125, - 73, - 174, - 173, - 132, - 25, - 135, - 55, - 200, - 136, - 205, - 241, + 254, + 99, + 202, + 61, 111, - 23, - 235, - 186, - 2, - 44, - 113, - 81, - 226, - 147, - 252, - 44, + 170, + 134, 146, - 149, + 196, + 190, + 64, + 105, + 209, + 210, + 237, + 186, 73, - 99 + 79, + 168, + 188, + 98, + 181, + 167 ], "score": { "structural_complexity": 19.9, "semantic_complexity": 17.5, - "duplication_ratio": 16.007393, + "duplication_ratio": 16.029411, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.407394, + "total": 93.42941, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -13108,15 +13108,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 70 duplicate code patterns" + "issue": "Found 72 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.9926064, + "amount": 3.9705882, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.0%" + "issue": "Code duplication: 19.9%" }, { "source_metric": "StructuralComplexity", @@ -44793,7 +44793,7 @@ }, "summary": { "total_files": 527, - "avg_score": 90.31896, + "avg_score": 90.319, "grade_distribution": { "A+": 194, "A": 176, diff --git a/src/runtime/eval_builtin_fs.rs b/src/runtime/eval_builtin_fs.rs index 7269b8a78..6edc59f21 100644 --- a/src/runtime/eval_builtin_fs.rs +++ b/src/runtime/eval_builtin_fs.rs @@ -487,10 +487,7 @@ pub(crate) fn eval_compute_hash(args: &[Value]) -> Result Err(InterpreterError::RuntimeError( diff --git a/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part1.rs b/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part1.rs index 82b7b68a1..a33facaa8 100644 --- a/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part1.rs +++ b/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part1.rs @@ -330,14 +330,11 @@ fn test_hash_deterministic() { let result1 = compute_hash_of("hello"); let result2 = compute_hash_of("hello"); assert_eq!(result1, result2); - assert_eq!( - result1, - "\"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\"" - ); + assert_eq!(result1, "\"5d41402abc4b2a76b9719d911017c592\""); } /// METHODS-1: `hash(x)` is neither an interpreter nor a transpiler builtin; -/// `compute_hash(path)` (SHA-256 of a file's bytes) is the existing equivalent. +/// `compute_hash(path)` (MD5 of a file's bytes, STDLIB-005) is the existing equivalent. pub(super) fn compute_hash_of(content: &str) -> String { let file = tempfile::NamedTempFile::new().expect("create temp file"); std::fs::write(file.path(), content).expect("write temp file"); diff --git a/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part2.rs b/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part2.rs index 148d9dec8..15a23e41f 100644 --- a/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part2.rs +++ b/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part2.rs @@ -783,22 +783,16 @@ fn test_sleep_float() { // === Hash Function === #[test] fn test_compute_hash_of_file_containing_hello() { - // METHODS-1: `hash(x)` does not exist; SHA-256 via `compute_hash` is deterministic + // METHODS-1: `hash(x)` does not exist; MD5 via `compute_hash` is deterministic let result = super::part1::compute_hash_of("hello"); - assert_eq!( - result, - "\"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\"" - ); + assert_eq!(result, "\"5d41402abc4b2a76b9719d911017c592\""); } #[test] fn test_compute_hash_of_file_containing_42() { // METHODS-1: `hash(x)` does not exist; hash the decimal text of 42 instead let result = super::part1::compute_hash_of("42"); - assert_eq!( - result, - "\"73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049\"" - ); + assert_eq!(result, "\"a1d0c6e83f027327d8461063f4ac58a6\""); } // === Additional String Methods via REPL === diff --git a/src/stdlib/hash.rs b/src/stdlib/hash.rs new file mode 100644 index 000000000..50811b365 --- /dev/null +++ b/src/stdlib/hash.rs @@ -0,0 +1,212 @@ +//! MD5 message digest (RFC 1321) behind the `compute_hash()` builtin (STDLIB-005). +//! +//! `compute_hash(path)` is a content fingerprint for duplicate detection, specified +//! as 32 lowercase hex digits of MD5. It is not a security primitive. The digest is +//! implemented in-tree so the builtin keeps that contract without an extra crate. + +/// Per-step left-rotation amounts (RFC 1321 §3.4). +const SHIFTS: [u32; 64] = [ + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, // + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, // + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, // + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, +]; + +/// Per-step additive constants `floor(|sin(i + 1)| * 2^32)` (RFC 1321 §3.4). +const K: [u32; 64] = [ + 0xd76a_a478, + 0xe8c7_b756, + 0x2420_70db, + 0xc1bd_ceee, + 0xf57c_0faf, + 0x4787_c62a, + 0xa830_4613, + 0xfd46_9501, + 0x6980_98d8, + 0x8b44_f7af, + 0xffff_5bb1, + 0x895c_d7be, + 0x6b90_1122, + 0xfd98_7193, + 0xa679_438e, + 0x49b4_0821, + 0xf61e_2562, + 0xc040_b340, + 0x265e_5a51, + 0xe9b6_c7aa, + 0xd62f_105d, + 0x0244_1453, + 0xd8a1_e681, + 0xe7d3_fbc8, + 0x21e1_cde6, + 0xc337_07d6, + 0xf4d5_0d87, + 0x455a_14ed, + 0xa9e3_e905, + 0xfcef_a3f8, + 0x676f_02d9, + 0x8d2a_4c8a, + 0xfffa_3942, + 0x8771_f681, + 0x6d9d_6122, + 0xfde5_380c, + 0xa4be_ea44, + 0x4bde_cfa9, + 0xf6bb_4b60, + 0xbebf_bc70, + 0x289b_7ec6, + 0xeaa1_27fa, + 0xd4ef_3085, + 0x0488_1d05, + 0xd9d4_d039, + 0xe6db_99e5, + 0x1fa2_7cf8, + 0xc4ac_5665, + 0xf429_2244, + 0x432a_ff97, + 0xab94_23a7, + 0xfc93_a039, + 0x655b_59c3, + 0x8f0c_cc92, + 0xffef_f47d, + 0x8584_5dd1, + 0x6fa8_7e4f, + 0xfe2c_e6e0, + 0xa301_4314, + 0x4e08_11a1, + 0xf753_7e82, + 0xbd3a_f235, + 0x2ad7_d2bb, + 0xeb86_d391, +]; + +/// Initial chaining values A, B, C, D (RFC 1321 §3.3). +const INIT: [u32; 4] = [0x6745_2301, 0xefcd_ab89, 0x98ba_dcfe, 0x1032_5476]; + +/// MD5 digest of `data` as 32 lowercase hex digits. +/// +/// # Examples +/// +/// ``` +/// assert_eq!( +/// ruchy::stdlib::hash::md5_hex(b"hello world"), +/// "5eb63bbbe01eeed093cb22bb8f5acdc3" +/// ); +/// ``` +#[must_use] +pub fn md5_hex(data: &[u8]) -> String { + md5_digest(data) + .iter() + .map(|b| format!("{b:02x}")) + .collect() +} + +/// Raw 16-byte MD5 digest of `data`. +#[must_use] +pub fn md5_digest(data: &[u8]) -> [u8; 16] { + let mut state = INIT; + for block in pad(data).chunks_exact(64) { + compress(&mut state, block); + } + let mut out = [0u8; 16]; + for (chunk, word) in out.chunks_exact_mut(4).zip(state) { + chunk.copy_from_slice(&word.to_le_bytes()); + } + out +} + +/// Append the 0x80 marker, zero fill to 56 mod 64, and the little-endian bit length. +fn pad(data: &[u8]) -> Vec { + let bit_len = (data.len() as u64).wrapping_mul(8); + let mut msg = Vec::with_capacity(data.len() + 72); + msg.extend_from_slice(data); + msg.push(0x80); + while msg.len() % 64 != 56 { + msg.push(0); + } + msg.extend_from_slice(&bit_len.to_le_bytes()); + msg +} + +/// Round function value and message-word index for step `i` (RFC 1321 §3.4). +fn step_fn(i: usize, b: u32, c: u32, d: u32) -> (u32, usize) { + match i / 16 { + 0 => ((b & c) | (!b & d), i), + 1 => ((d & b) | (!d & c), (5 * i + 1) % 16), + 2 => (b ^ c ^ d, (3 * i + 5) % 16), + _ => (c ^ (b | !d), (7 * i) % 16), + } +} + +/// Fold one 64-byte block into the chaining state. +fn compress(state: &mut [u32; 4], block: &[u8]) { + let mut words = [0u32; 16]; + for (word, bytes) in words.iter_mut().zip(block.chunks_exact(4)) { + *word = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + } + let [mut a, mut b, mut c, mut d] = *state; + for i in 0..64 { + let (f, g) = step_fn(i, b, c, d); + let sum = a.wrapping_add(f).wrapping_add(K[i]).wrapping_add(words[g]); + a = d; + d = c; + c = b; + b = b.wrapping_add(sum.rotate_left(SHIFTS[i])); + } + for (s, v) in state.iter_mut().zip([a, b, c, d]) { + *s = s.wrapping_add(v); + } +} + +#[cfg(test)] +mod tests { + use super::md5_hex; + + /// RFC 1321 Appendix A.5 test suite. + #[test] + fn test_stdlib_005_md5_rfc1321_suite() { + let cases: [(&[u8], &str); 7] = [ + (b"", "d41d8cd98f00b204e9800998ecf8427e"), + (b"a", "0cc175b9c0f1b6a831c399e269772661"), + (b"abc", "900150983cd24fb0d6963f7d28e17f72"), + (b"message digest", "f96b697d7cb7938d525a2f31aaf161d0"), + ( + b"abcdefghijklmnopqrstuvwxyz", + "c3fcd3d76192e4007dfb496cca67e13b", + ), + ( + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "d174ab98d277d9f5a5611c2c9f419d9f", + ), + ( + b"12345678901234567890123456789012345678901234567890123456789012345678901234567890", + "57edf4a22be3c955ac49da2e2107b67a", + ), + ]; + for (input, expected) in cases { + assert_eq!(md5_hex(input), expected, "input {input:?}"); + } + } + + /// Padding boundaries: 55 bytes fits one block, 56 and 64 need a second. + #[test] + fn test_stdlib_005_md5_padding_boundaries() { + assert_eq!(md5_hex(&[b'a'; 55]), "ef1772b6dff9a122358552954ad0df65"); + assert_eq!(md5_hex(&[b'a'; 56]), "3b0c8ac703f828b04c6c197006d17218"); + assert_eq!(md5_hex(&[b'a'; 64]), "014842d480b571495a4a0363793f7367"); + } + + mod properties { + use super::super::md5_hex; + use proptest::prelude::*; + + proptest! { + #[test] + fn prop_stdlib_005_md5_is_32_lower_hex(data in proptest::collection::vec(any::(), 0..300)) { + let hex = md5_hex(&data); + prop_assert_eq!(hex.len(), 32); + prop_assert!(hex.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))); + } + } + } +} diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs index 7578a5eb8..19977fea3 100644 --- a/src/stdlib/mod.rs +++ b/src/stdlib/mod.rs @@ -58,6 +58,7 @@ pub mod simular_bridge; // Pillar 7: Simulation Engine // Pillar 4: Shell Transp // === Utility Modules === pub mod env; pub mod fs; +pub mod hash; pub mod json; pub mod logging; pub mod path; From 33cc06b33c755e319c8314ddb0845118ad1edf9f Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:52:58 +0200 Subject: [PATCH 008/106] G2B-S5: readme, dogfood pending allowlist, renacer version floor - readme_validation: README title is

Ruchy

and the usage section is '## Core Commands'; test accepts those spellings (sections exist) - dogfood_gates_declared: scripts/release-policy.sh landed (PMAT-135); pending allowlist emptied as the test itself instructs - tooling_002: 0.6 is a minimum (traces from 0.6.2 read fine on 0.10.2; all other renacer tests pass), parsed as a (major, minor) floor, not a substring pin Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 211 +++++++++++++++-------- tests/dogfood_gates_declared.rs | 11 +- tests/readme_validation.rs | 20 ++- tests/tooling_002_renacer_integration.rs | 18 +- 4 files changed, 174 insertions(+), 86 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 5106ef811..f18746134 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:14.898957255Z", + "created_at": "2026-09-24T00:02:16.539162727Z", "git_context": null, "files": { "./build.rs": { @@ -33806,48 +33806,48 @@ }, "./src/runtime/eval_builtin_fs.rs": { "content_hash": [ - 134, - 158, - 181, - 66, + 163, + 130, + 120, + 222, + 168, + 53, + 74, + 214, + 145, + 154, + 187, + 239, + 71, + 245, 252, - 0, - 50, - 254, - 218, - 173, - 29, - 127, - 247, - 169, - 127, - 152, - 204, - 89, + 175, + 132, + 111, 96, - 188, - 166, - 254, - 113, - 199, - 152, - 22, - 194, - 210, - 224, - 48, - 253, - 98 + 156, + 196, + 158, + 94, + 79, + 162, + 42, + 205, + 107, + 216, + 60, + 65, + 128 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.029411, + "duplication_ratio": 15.007385, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.02941, + "total": 75.007385, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33863,11 +33863,11 @@ }, { "source_metric": "Duplication", - "amount": 4.970588, + "amount": 4.9926143, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 24.9%" + "issue": "Code duplication: 25.0%" }, { "source_metric": "StructuralComplexity", @@ -41554,6 +41554,77 @@ }, "git_context": null }, + "./src/stdlib/hash.rs": { + "content_hash": [ + 245, + 136, + 56, + 33, + 87, + 85, + 153, + 67, + 145, + 119, + 239, + 236, + 114, + 249, + 67, + 111, + 131, + 234, + 2, + 234, + 140, + 58, + 157, + 139, + 238, + 1, + 24, + 139, + 216, + 107, + 167, + 149 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 7.0, + "total": 97.27273, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/stdlib/hash.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 3.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 6 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/stdlib/html.rs": { "content_hash": [ 79, @@ -41880,38 +41951,38 @@ }, "./src/stdlib/mod.rs": { "content_hash": [ - 55, - 9, - 236, - 68, - 184, - 210, - 158, - 74, - 228, - 40, - 201, - 244, - 244, + 117, + 213, + 217, + 29, + 113, + 111, + 139, + 37, + 26, + 212, + 6, + 155, 73, - 115, - 239, - 105, - 78, - 201, - 222, - 220, - 143, - 120, - 4, - 39, - 144, - 60, - 209, - 122, - 43, - 181, - 140 + 91, + 185, + 32, + 197, + 242, + 134, + 168, + 99, + 86, + 184, + 241, + 140, + 194, + 103, + 47, + 69, + 35, + 172, + 190 ], "score": { "structural_complexity": 25.0, @@ -44792,10 +44863,10 @@ } }, "summary": { - "total_files": 527, - "avg_score": 90.319, + "total_files": 528, + "avg_score": 90.33213, "grade_distribution": { - "A+": 194, + "A+": 195, "A": 176, "A-": 31, "B+": 30, @@ -44807,7 +44878,7 @@ "languages": { "JavaScript": 14, "Python": 4, - "Rust": 505, + "Rust": 506, "TypeScript": 4 } } diff --git a/tests/dogfood_gates_declared.rs b/tests/dogfood_gates_declared.rs index 05bfbf87a..e3f0e87fa 100644 --- a/tests/dogfood_gates_declared.rs +++ b/tests/dogfood_gates_declared.rs @@ -15,13 +15,12 @@ use std::process::Command; /// Gates declared here but owned by another in-flight ticket. /// -/// `scripts/release-policy.sh` is written by PMAT-135 on its own branch. Until -/// that branch merges the file is absent from this worktree, and the -/// declared-but-missing rule would fire on it for a reason that is not a defect -/// of this tree. The allowlist is deliberately narrow: one name, and -/// `test_pmat_136_pending_allowlist_is_empty_once_the_gate_lands` deletes the +/// Empty: `scripts/release-policy.sh` (PMAT-135) has landed, so every declared +/// gate is again covered by the declared-but-missing rule. A future entry must +/// name a gate owned by an in-flight ticket; +/// `test_pmat_136_pending_allowlist_is_empty_once_the_gate_lands` removes the /// exemption the moment the file appears. -const PENDING_FROM_OTHER_TICKETS: [&str; 1] = ["scripts/release-policy.sh"]; +const PENDING_FROM_OTHER_TICKETS: [&str; 0] = []; /// The crate root, independent of the caller's working directory. fn repo_root() -> PathBuf { diff --git a/tests/readme_validation.rs b/tests/readme_validation.rs index aba5f75fa..1a50a3127 100644 --- a/tests/readme_validation.rs +++ b/tests/readme_validation.rs @@ -77,17 +77,21 @@ fn test_readme_not_empty() { fn test_readme_required_sections() { let content = fs::read_to_string("README.md").expect("Failed to read README.md"); - let required_sections = vec![ - "# Ruchy", // Title - "## Features", // What it does - "## Installation", // How to install - "## CLI Commands", // How to use + // Each required section with its accepted spellings: the title is an + // HTML `

` in the centered header block, and the CLI + // usage section is headed "Core Commands". + let required_sections: [&[&str]; 4] = [ + &["# Ruchy", ">Ruchy

"], // Title + &["## Features"], // What it does + &["## Installation"], // How to install + &["## CLI Commands", "## Core Commands"], // How to use ]; - for section in required_sections { + for spellings in required_sections { assert!( - content.contains(section), - "README.md must contain section: {section}" + spellings.iter().any(|s| content.contains(s)), + "README.md must contain section: {}", + spellings.join(" or ") ); } } diff --git a/tests/tooling_002_renacer_integration.rs b/tests/tooling_002_renacer_integration.rs index d21670e2b..7c729b278 100644 --- a/tests/tooling_002_renacer_integration.rs +++ b/tests/tooling_002_renacer_integration.rs @@ -36,12 +36,26 @@ fn test_tooling_002_01_renacer_installed() { stdout.contains("renacer"), "renacer version output should contain 'renacer'" ); + // Golden traces were captured with renacer 0.6.2; later releases read the + // same traces (tests 2-N pass on 0.10.x), so 0.6 is a floor, not a pin. assert!( - stdout.contains("0.6"), - "renacer should be v0.6.x, got: {stdout}" + renacer_version_at_least(&stdout, (0, 6)), + "renacer should be v0.6 or newer, got: {stdout}" ); } +/// Parse `renacer X.Y.Z` and compare (major, minor) against `floor`. +fn renacer_version_at_least(version_output: &str, floor: (u64, u64)) -> bool { + let Some(version) = version_output.split_whitespace().nth(1) else { + return false; + }; + let mut parts = version.split('.').map(|p| p.parse::().ok()); + match (parts.next().flatten(), parts.next().flatten()) { + (Some(major), Some(minor)) => (major, minor) >= floor, + _ => false, + } +} + /// Test 2: Verify golden-traces directory exists #[test] fn test_tooling_002_02_golden_traces_dir_exists() { From fffced9226b2b17ec28c104eb21ce10a017e06eb Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 20:53:49 +0200 Subject: [PATCH 009/106] G2B-S5b: cli_contract_sovereign asserts the coverage gate fails below threshold; version 5.0.x - contracts check --min-coverage 80 on a contract-free file reports 0.0% and exits 1 (correct gate behaviour); test asserted success - version test pinned 5.0.0-alpha; crate is 5.0.0-beta.2 -> assert 'ruchy 5.0.' Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2 +- tests/cli_contract_sovereign.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index f18746134..fe613a606 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:16.539162727Z", + "created_at": "2026-09-24T00:02:18.325613548Z", "git_context": null, "files": { "./build.rs": { diff --git a/tests/cli_contract_sovereign.rs b/tests/cli_contract_sovereign.rs index 3312f8019..825040ed5 100644 --- a/tests/cli_contract_sovereign.rs +++ b/tests/cli_contract_sovereign.rs @@ -511,8 +511,10 @@ fn test_contracts_check_with_threshold() { .arg("--min-coverage") .arg("80") .assert() - .success() - .stdout(predicate::str::contains("80.0%")); + // `fn main() {}` carries no contract: 0.0% < 80.0% must fail the gate. + .failure() + .stdout(predicate::str::contains("80.0%")) + .stderr(predicate::str::contains("below threshold 80.0%")); } // ============================================================================ @@ -558,5 +560,5 @@ fn test_version_is_5_0() { .arg("--version") .assert() .success() - .stdout(predicate::str::contains("5.0.0-alpha")); + .stdout(predicate::str::contains("ruchy 5.0.")); } From ec9cb02f4361f0797bf1dc8ffcad8d82c63e793c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 21:07:50 +0200 Subject: [PATCH 010/106] G2B-T7: trait tests use valid Ruchy syntax; associated-type transpile test enabled - parser_defect_010: the default-method body was f"...", x) (a stray ')' after an f-string); the parser rightly rejects it. Body is now the spec's interpolated form f"(Read more from {self.summarize_author()}...)". - sqlite_163: 'fun next(self): Option' is not Ruchy syntax (SPECIFICATION uses '-> Option'); now '-> Option'. - test_parser_010_transpile un-ignored: trait associated types already transpile. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2 +- tests/parser_defect_010_trait_associated_types.rs | 5 ++--- tests/sqlite_001_parser_grammar.rs | 5 +++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index fe613a606..f20043e6f 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:18.325613548Z", + "created_at": "2026-09-24T00:02:20.651624481Z", "git_context": null, "files": { "./build.rs": { diff --git a/tests/parser_defect_010_trait_associated_types.rs b/tests/parser_defect_010_trait_associated_types.rs index dc75a01d9..671c7e4be 100644 --- a/tests/parser_defect_010_trait_associated_types.rs +++ b/tests/parser_defect_010_trait_associated_types.rs @@ -73,7 +73,7 @@ trait Summary { fn summarize_author(&self) -> String fn summarize(&self) -> String { - f"(Read more from {}...)", self.summarize_author()) + f"(Read more from {self.summarize_author()}...)" } } "#; @@ -120,7 +120,7 @@ trait Summary { fn summarize_author(&self) -> String fn summarize(&self) -> String { - f"(Read more from {}...)", self.summarize_author()) + f"(Read more from {self.summarize_author()}...)" } } @@ -147,7 +147,6 @@ trait From { // Test 7: Transpile trait with associated type #[test] -#[ignore = "Trait associated types not fully transpiled yet"] fn test_parser_010_transpile() { let code = r" trait Iterator { diff --git a/tests/sqlite_001_parser_grammar.rs b/tests/sqlite_001_parser_grammar.rs index d18b1d157..f3ae5a456 100644 --- a/tests/sqlite_001_parser_grammar.rs +++ b/tests/sqlite_001_parser_grammar.rs @@ -8280,12 +8280,13 @@ fn test_sqlite_162_where_clauses() { #[test] fn test_sqlite_163_associated_types() { - // Associated types + // Associated types, in SPECIFICATION.md syntax (`-> Option`, cf. §actors + // `-> Option`); a `): T` return type is not Ruchy syntax. assert_parses( r" trait Iterator { type Item - fun next(self): Option + fun next(self) -> Option } ", ); From d2198c2ba3aae9edd93dca3762275c84e824caa3 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 21:13:00 +0200 Subject: [PATCH 011/106] G2B-S2: command router is exhaustive; prove is routed, no silent 'not yet implemented' - handle_complex_command sends Commands::Prove to dispatch_analysis (it was only listed there, so it fell through to dispatch_tooling's wildcard and exited 0). - dispatch_tooling's wildcard arm (eprintln + Ok) is gone: the match names every Commands variant, so an unrouted new variant no longer compiles. Variants main or dispatch_analysis own return an internal routing error. - tests/g2b_s2_cli.rs walks every verb in `ruchy --help` (recursing into command groups) on a trivial input; none may exit 0 printing the stub line. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2 +- src/bin/handlers/command_router.rs | 100 +++++++++++++++-- tests/g2b_s2_cli.rs | 166 +++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 tests/g2b_s2_cli.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index f20043e6f..2ac015dc1 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:20.651624481Z", + "created_at": "2026-09-24T00:02:23.621914046Z", "git_context": null, "files": { "./build.rs": { diff --git a/src/bin/handlers/command_router.rs b/src/bin/handlers/command_router.rs index 280361d2d..ef065197d 100644 --- a/src/bin/handlers/command_router.rs +++ b/src/bin/handlers/command_router.rs @@ -67,7 +67,8 @@ pub fn handle_complex_command(command: crate::Commands) -> Result<()> { | crate::Commands::QualityGate { .. } | crate::Commands::Fmt { .. } | crate::Commands::Lint { .. } - | crate::Commands::Coverage { .. }) => dispatch_analysis(cmd), + | crate::Commands::Coverage { .. } + | crate::Commands::Prove { .. }) => dispatch_analysis(cmd), // Interactive and tooling commands cmd => dispatch_tooling(cmd), } @@ -276,7 +277,9 @@ fn dispatch_analysis(command: crate::Commands) -> Result<()> { &format, verbose, ), - _ => unreachable!("dispatch_analysis called with non-analysis command"), + _ => Err(routing_error( + "dispatch_analysis received a non-analysis command", + )), } } @@ -634,13 +637,49 @@ fn dispatch_tooling(command: crate::Commands) -> Result<()> { } => crate::handlers::handlers_modules::sovereign::handle_suggest_contracts( &path, &format, verbose, pub_only, &exclude, ), - _ => { - eprintln!("Command not yet implemented"); - Ok(()) - } + // Exhaustive on purpose (G2B-S2): no wildcard arm, so a new `Commands` + // variant does not compile until it is routed somewhere. + crate::Commands::Ast { .. } + | crate::Commands::Provability { .. } + | crate::Commands::Runtime { .. } + | crate::Commands::Score { .. } + | crate::Commands::QualityGate { .. } + | crate::Commands::Fmt { .. } + | crate::Commands::Lint { .. } + | crate::Commands::Coverage { .. } + | crate::Commands::Prove { .. } => Err(routing_error( + "an analysis command reached dispatch_tooling", + )), + crate::Commands::Repl { .. } + | crate::Commands::New { .. } + | crate::Commands::Build { .. } + | crate::Commands::Publish { .. } + | crate::Commands::Parse { .. } + | crate::Commands::Transpile { .. } + | crate::Commands::Run { .. } + | crate::Commands::Compile { .. } + | crate::Commands::Check { .. } + | crate::Commands::Fix { .. } + | crate::Commands::Convert { .. } + | crate::Commands::Vocab { .. } + | crate::Commands::Test { .. } + | crate::Commands::PropertyTests { .. } + | crate::Commands::Mutations { .. } + | crate::Commands::Fuzz { .. } + | crate::Commands::Oracle { .. } + | crate::Commands::Hunt { .. } + | crate::Commands::Report { .. } => Err(routing_error( + "this command is dispatched by main, not the command router", + )), } } +/// An internal CLI routing defect: a command reached a dispatcher that does not +/// own it. Returned as an error (never a silent `Ok`) so the process exits non-zero. +fn routing_error(detail: &str) -> anyhow::Error { + anyhow::anyhow!("internal CLI routing error: {detail}") +} + #[cfg(test)] mod tests { use super::*; @@ -783,4 +822,53 @@ mod tests { let err = validate_lint_args(None, false).unwrap_err(); assert!(err.contains("file") || err.contains("--all")); } + + // ======================================================================== + // G2B-S2 (RHLGA-1): exhaustive routing, no silent catch-all + // ======================================================================== + + fn missing_path() -> std::path::PathBuf { + let p = std::env::temp_dir().join(format!("ruchy-router-missing-{}", std::process::id())); + assert!(!p.exists(), "{} unexpectedly exists", p.display()); + p + } + + fn prove_check(file: std::path::PathBuf) -> crate::Commands { + crate::Commands::Prove { + file: Some(file), + backend: "z3".to_string(), + ml_suggestions: false, + timeout: 5000, + script: None, + export: None, + check: true, + counterexample: false, + verbose: false, + format: "text".to_string(), + } + } + + #[test] + fn test_command_router_prove_reaches_the_prover() { + // A missing file is an error only if the prover actually ran. + let result = handle_complex_command(prove_check(missing_path())); + assert!(result.is_err(), "prove must be routed, not swallowed"); + } + + #[test] + fn test_command_router_prove_check_on_valid_file_succeeds() { + let file = tempfile::NamedTempFile::new().expect("temp file"); + std::fs::write(file.path(), "let x = 42").expect("write"); + let result = handle_complex_command(prove_check(file.path().to_path_buf())); + assert!(result.is_ok(), "prove --check failed: {result:?}"); + } + + #[test] + fn test_command_router_main_routed_variant_is_an_error_not_a_silent_ok() { + let result = handle_complex_command(crate::Commands::Parse { + file: missing_path(), + }); + let err = result.expect_err("a variant main dispatches must not succeed here"); + assert!(err.to_string().contains("routing"), "got: {err}"); + } } diff --git a/tests/g2b_s2_cli.rs b/tests/g2b_s2_cli.rs new file mode 100644 index 000000000..9e2ff5e95 --- /dev/null +++ b/tests/g2b_s2_cli.rs @@ -0,0 +1,166 @@ +#![allow(missing_docs)] +//! G2B-S2 (RHLGA-1): no CLI verb may silently succeed without doing its job. +//! +//! The command router used to end in `_ => { eprintln!("Command not yet +//! implemented"); Ok(()) }`, so any `Commands` variant it did not route (e.g. +//! `prove`) printed that line and exited 0. These tests walk every verb that +//! `ruchy --help` lists (recursing into nested command groups) and run it on a +//! trivial input: it must either work or exit non-zero. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; +use tempfile::TempDir; + +const STUB_MARKER: &str = "not yet implemented"; +const TRIVIAL_SOURCE: &str = "let x = 42\nprintln(x)\n"; +const PER_VERB_TIMEOUT: Duration = Duration::from_secs(30); + +fn ruchy_cmd() -> Command { + assert_cmd::cargo::cargo_bin_cmd!("ruchy") +} + +/// Subcommand names from the `Commands:` section of a clap help text. +fn subcommands_in_help(help: &str) -> Vec { + help.lines() + .skip_while(|line| line.trim() != "Commands:") + .skip(1) + .take_while(|line| line.starts_with(" ")) + .filter_map(|line| line.split_whitespace().next()) + .filter(|name| *name != "help") + .map(str::to_string) + .collect() +} + +fn help_of(path: &[String]) -> String { + let output = ruchy_cmd() + .args(path) + .arg("--help") + .output() + .expect("run ruchy --help"); + assert!(output.status.success(), "`ruchy {path:?} --help` failed"); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +/// Every leaf verb path, e.g. `["prove"]`, `["infra", "destroy"]`. +fn leaf_verbs(prefix: &[String]) -> Vec> { + let children = subcommands_in_help(&help_of(prefix)); + if children.is_empty() { + return vec![prefix.to_vec()]; + } + children + .into_iter() + .flat_map(|child| { + let mut path = prefix.to_vec(); + path.push(child); + leaf_verbs(&path) + }) + .collect() +} + +/// `None` when the verb works (exit 0 without the stub marker), is still +/// running at the timeout (servers, REPLs), or fails loudly (non-zero exit). +/// `Some(report)` when it exits 0 while printing the stub marker. +fn silent_stub(verb: &[String], dir: &Path) -> Option { + let output = match ruchy_cmd() + .current_dir(dir) + .args(verb) + .arg("main.ruchy") + .write_stdin("") + .timeout(PER_VERB_TIMEOUT) + .output() + { + Ok(output) => output, + Err(_) => return None, + }; + let text = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let is_stub = output.status.success() && text.to_lowercase().contains(STUB_MARKER); + is_stub.then(|| { + format!( + "`ruchy {}` exited 0 printing {STUB_MARKER:?}", + verb.join(" ") + ) + }) +} + +fn scratch_dir() -> TempDir { + let dir = TempDir::new().expect("tempdir"); + std::fs::write(dir.path().join("main.ruchy"), TRIVIAL_SOURCE).expect("write main.ruchy"); + dir +} + +#[test] +fn test_g2b_s2_help_lists_the_expected_verbs() { + let verbs = subcommands_in_help(&help_of(&[])); + for expected in ["prove", "infra", "serve", "run", "transpile"] { + assert!( + verbs.iter().any(|v| v == expected), + "`ruchy --help` should list `{expected}`: {verbs:?}" + ); + } +} + +#[test] +fn test_g2b_s2_no_verb_silently_succeeds_as_a_stub() { + let verbs = leaf_verbs(&[]); + assert!( + verbs.len() > 40, + "expected the full verb tree, got {verbs:?}" + ); + let failures: Vec = std::thread::scope(|scope| { + let handles: Vec<_> = verbs + .chunks(verbs.len().div_ceil(8)) + .map(|chunk| { + scope.spawn(move || { + chunk + .iter() + .filter_map(|verb| silent_stub(verb, scratch_dir().path())) + .collect::>() + }) + }) + .collect(); + handles + .into_iter() + .flat_map(|h| h.join().expect("verb worker panicked")) + .collect() + }); + assert!( + failures.is_empty(), + "silent stubs:\n{}", + failures.join("\n") + ); +} + +#[test] +fn test_g2b_s2_prove_check_is_routed_to_the_prover() { + let dir = scratch_dir(); + let output = ruchy_cmd() + .current_dir(dir.path()) + .args(["prove", "main.ruchy", "--check"]) + .write_stdin("") + .timeout(PER_VERB_TIMEOUT) + .output() + .expect("ruchy prove"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.to_lowercase().contains(STUB_MARKER), + "prove must reach the prover, got stderr: {stderr}" + ); + assert_eq!(output.status.code(), Some(0), "stderr: {stderr}"); +} + +#[test] +fn test_g2b_s2_prove_missing_file_fails() { + let dir = TempDir::new().expect("tempdir"); + ruchy_cmd() + .current_dir(dir.path()) + .args(["prove", "missing.ruchy", "--check"]) + .write_stdin("") + .timeout(PER_VERB_TIMEOUT) + .assert() + .failure(); +} From f75b2719491ed581f4359740b48d1a7328cab9d5 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 21:15:38 +0200 Subject: [PATCH 012/106] G2B-T5: `::` callees are recorded in the AST; a `.` callee emits (obj.f)(..) The parser marks a FieldAccess written with `::` (Expr::is_path_access, attribute "::", not spellable in source); the interpreter still sees a plain FieldAccess. The transpiler renders a marked chain as a Rust path and wraps an unmarked FieldAccess callee in parens, replacing the G2B-T1 lowercase-root heuristic. tests/g2b_t2a_receiver_and_paths.rs: module path, std::cmp::max, Point::new, closure field (rustc-run); hand mutation is_path_access=false -> 2 RED. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 72 ++++---- src/backend/transpiler/call_transpilation.rs | 37 ++-- src/frontend/ast.rs | 14 ++ src/frontend/parser/mod.rs | 10 +- tests/g2b_t2a_receiver_and_paths.rs | 174 +++++++++++++++++++ 5 files changed, 256 insertions(+), 51 deletions(-) create mode 100644 tests/g2b_t2a_receiver_and_paths.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 2ac015dc1..21176bbbc 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:23.621914046Z", + "created_at": "2026-09-24T00:02:25.956192176Z", "git_context": null, "files": { "./build.rs": { @@ -10454,48 +10454,48 @@ }, "./src/bin/handlers/command_router.rs": { "content_hash": [ - 188, - 7, - 223, - 224, - 33, - 164, - 2, - 91, - 61, - 174, - 99, - 138, + 85, + 10, + 207, + 121, + 119, + 104, + 93, 253, - 4, + 27, + 136, + 140, + 68, + 48, 18, - 96, - 62, - 69, - 79, - 64, - 198, - 240, - 75, - 239, + 242, + 163, + 177, + 189, + 110, + 162, + 80, 52, - 57, - 245, - 219, - 209, - 98, - 154, - 153 + 147, + 160, + 122, + 34, + 94, + 159, + 114, + 162, + 94, + 174 ], "score": { "structural_complexity": 8.2, "semantic_complexity": 20.0, - "duplication_ratio": 17.793493, + "duplication_ratio": 17.794872, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 85.99349, + "total": 85.99487, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -10507,11 +10507,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 121 duplicate code patterns" + "issue": "Found 130 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.2065065, + "amount": 2.2051282, "applied_to": [ "Duplication" ], @@ -10531,7 +10531,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 66" + "issue": "High cyclomatic complexity: 67" } ], "critical_defects_count": 0, @@ -44864,7 +44864,7 @@ }, "summary": { "total_files": 528, - "avg_score": 90.33213, + "avg_score": 90.33214, "grade_distribution": { "A+": 195, "A": 176, diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index 591d4ac2a..242cf6db3 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -36,26 +36,26 @@ impl Transpiler { self.transpile_regular_function_call(&func_tokens, args) } - /// PARSER-094: Render a callee `a::b::c` (a FieldAccess chain rooted at a - /// lowercase module identifier) as a Rust path. `self.f` is a field, not a path. + /// PARSER-094 / G2B-T5: Render a callee written `a::b::c` as a Rust path. + /// The parser marks each `::` segment (`Expr::is_path_access`); a `.` access + /// is never part of a path. fn module_call_path(func: &Expr) -> Option { - let segments = Self::callee_path_segments(func)?; - let root = segments.first()?; - let is_module_root = - root.starts_with(|c: char| c.is_ascii_lowercase()) && root != "self" && root != "std"; - if segments.len() < 2 || !is_module_root { + if !func.is_path_access() { return None; } + let segments = Self::callee_path_segments(func)?; let idents = segments.iter().map(|s| format_ident!("{}", s)); Some(quote! { #(#idents)::* }) } - /// Collect the identifier segments of a FieldAccess chain; None if any - /// segment is not a plain identifier (tuple index, call, literal, ...). + /// Collect the identifier segments of a `::` chain; None if any segment is + /// not a plain identifier or is joined by `.` instead of `::`. fn callee_path_segments(expr: &Expr) -> Option> { match &expr.kind { ExprKind::Identifier(name) if Self::is_plain_ident(name) => Some(vec![name.clone()]), - ExprKind::FieldAccess { object, field } if Self::is_plain_ident(field) => { + ExprKind::FieldAccess { object, field } + if expr.is_path_access() && Self::is_plain_ident(field) => + { let mut segments = Self::callee_path_segments(object)?; segments.push(field.clone()); Some(segments) @@ -69,6 +69,17 @@ impl Transpiler { && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') } + /// G2B-T5: a `.` callee is a field holding a callable, called as `(obj.f)(..)`; + /// Rust would read `obj.f(..)` as a method call. + fn parenthesize_field_callee(func: &Expr, tokens: TokenStream) -> TokenStream { + let is_field = matches!(func.kind, ExprKind::FieldAccess { .. }) && !func.is_path_access(); + if is_field { + quote! { (#tokens) } + } else { + tokens + } + } + /// Transform main() calls to __ruchy_main() fn transform_main_call(&self, func: &Expr) -> Result { if let ExprKind::Identifier(name) = &func.kind { @@ -77,12 +88,12 @@ impl Transpiler { return Ok(quote! { #renamed_ident }); } } - // PARSER-094 (Issue #137): `module::function(..)` parses as Call{FieldAccess} - // (`obj.method(..)` is a MethodCall), so a callee path keeps `::`. + // PARSER-094 (Issue #137): a `module::function(..)` callee keeps `::`. if let Some(path_tokens) = Self::module_call_path(func) { return Ok(path_tokens); } - self.transpile_expr(func) + let tokens = self.transpile_expr(func)?; + Ok(Self::parenthesize_field_callee(func, tokens)) } /// Try to transpile std::time::now_millis() calls diff --git a/src/frontend/ast.rs b/src/frontend/ast.rs index 7be0fdceb..a56e6afdd 100644 --- a/src/frontend/ast.rs +++ b/src/frontend/ast.rs @@ -274,6 +274,12 @@ pub struct Expr { /// At Silver level (default), they transpile to `debug_assert!` in Rust. pub contracts: Vec, } +/// Attribute name the parser attaches to a `FieldAccess` written with `::` +/// (`module::function`, `Type::assoc`). It is not spellable in source, so a +/// `.` access (`obj.field`) never carries it. Consumers that do not care about +/// the distinction (the interpreter) see an ordinary `FieldAccess`. +pub const PATH_ACCESS_MARKER: &str = "::"; + impl Expr { /// Creates a new expression with the given kind and span. /// @@ -295,6 +301,14 @@ impl Expr { /// Span::new(0, 4) /// ); /// ``` + /// True when this expression is a `FieldAccess` written with `::` (a path + /// segment), false for `obj.field` and every other expression. + #[must_use] + pub fn is_path_access(&self) -> bool { + matches!(self.kind, ExprKind::FieldAccess { .. }) + && self.attributes.iter().any(|a| a.name == PATH_ACCESS_MARKER) + } + #[must_use] pub fn new(kind: ExprKind, span: Span) -> Self { Self { diff --git a/src/frontend/parser/mod.rs b/src/frontend/parser/mod.rs index f5e0a38e0..4bf10cc4b 100644 --- a/src/frontend/parser/mod.rs +++ b/src/frontend/parser/mod.rs @@ -585,13 +585,19 @@ fn handle_colon_colon_operator(state: &mut ParserState, left: Expr) -> Result diff --git a/tests/g2b_t2a_receiver_and_paths.rs b/tests/g2b_t2a_receiver_and_paths.rs new file mode 100644 index 000000000..92e147c1e --- /dev/null +++ b/tests/g2b_t2a_receiver_and_paths.rs @@ -0,0 +1,174 @@ +//! G2B-T5 (RHLGA-1): a `::` callee is a path, a `.` callee is a field. +//! +//! The parser records `a::b` as a FieldAccess marked with `Expr::is_path_access`, +//! so the transpiler emits `a::b(..)` for a path callee and `(obj.f)(..)` for a +//! field callee (a closure/fn pointer stored in a struct field). Each program is +//! transpiled, compiled with rustc and run; its output is compared with +//! `ruchy run` where the interpreter accepts the program. + +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] + +use ruchy::frontend::ast::{Expr, ExprKind}; +use ruchy::{Parser, Transpiler}; +use std::path::Path; +use std::process::Command; + +fn scratch_dir(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("g2b_t2a_{}_{name}", std::process::id())); + std::fs::create_dir_all(&dir).expect("scratch dir"); + dir +} + +fn compile_and_run(rust: &str, name: &str) -> String { + let dir = scratch_dir(name); + let src = dir.join("main.rs"); + let bin = dir.join("main"); + std::fs::write(&src, rust).expect("write rust"); + let out = Command::new("rustc") + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&src) + .output() + .expect("rustc runs"); + assert!( + out.status.success(), + "rustc rejected the transpiled program:\n{}\n--- rust ---\n{rust}", + String::from_utf8_lossy(&out.stderr) + ); + let run = Command::new(&bin).output().expect("binary runs"); + assert!(run.status.success(), "binary failed: {run:?}"); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +fn transpile(ast: &Expr) -> String { + Transpiler::new() + .transpile_to_program(ast) + .expect("transpiles") + .to_string() +} + +fn ruchy_run(code: &str, name: &str) -> String { + let file = scratch_dir(name).join("prog.ruchy"); + std::fs::write(&file, code).expect("write ruchy"); + let out = Command::new(env!("CARGO_BIN_EXE_ruchy")) + .arg("run") + .arg(Path::new(&file)) + .output() + .expect("ruchy runs"); + assert!(out.status.success(), "ruchy run failed: {out:?}"); + String::from_utf8_lossy(&out.stdout).into_owned() +} + +/// Transpile, compile, run, and compare with the interpreter. +fn differential(code: &str, name: &str, expected: &str) -> String { + let ast = Parser::new(code).parse().expect("parses"); + let rust = transpile(&ast); + assert_eq!(compile_and_run(&rust, name).trim(), expected, "{rust}"); + assert_eq!(ruchy_run(code, name).trim(), expected); + rust +} + +#[test] +fn test_g2b_t5_01_module_path_call() { + let code = r#" +mod helpers { + pub fun double(x: i32) -> i32 { x * 2 } +} +fun main() { + println!("{}", helpers::double(4)) +} +"#; + let rust = differential(code, "module", "8"); + assert!(rust.contains("helpers :: double"), "{rust}"); +} + +/// A three-segment path `std::cmp::max(..)`, compiled with rustc. The +/// interpreter has no `std::cmp` (`Object has no field named 'cmp'`) and a +/// nested `pub mod`/`pub struct` inside a `mod` does not parse or transpile, so +/// this case is transpiler-only. +#[test] +fn test_g2b_t5_02_nested_path_call() { + let code = r#" +fun main() { + println!("{}", std::cmp::max(3, 7)) +} +"#; + let ast = Parser::new(code).parse().expect("parses"); + let rust = transpile(&ast); + assert!(rust.contains("std :: cmp :: max (3"), "{rust}"); + assert_eq!(compile_and_run(&rust, "nested").trim(), "7", "{rust}"); +} + +#[test] +fn test_g2b_t5_03_associated_function_call() { + let code = r#" +struct Point { x: i32, y: i32 } +impl Point { + pub fun new(x: i32, y: i32) -> Point { Point { x: x, y: y } } +} +fun main() { + let p = Point::new(1, 2) + println!("{}", p.x + p.y) +} +"#; + let rust = differential(code, "assoc", "3"); + assert!(rust.contains("Point :: new"), "{rust}"); +} + +/// Replace the callee `probe` of every Call with `object.field` (a `.` access). +fn retarget_probe(expr: &mut Expr, object: &str, field: &str) { + if let ExprKind::Call { func, .. } = &mut expr.kind { + if matches!(&func.kind, ExprKind::Identifier(n) if n == "probe") { + let obj = Expr::new(ExprKind::Identifier(object.to_string()), func.span); + let access = ExprKind::FieldAccess { + object: Box::new(obj), + field: field.to_string(), + }; + **func = Expr::new(access, func.span); + } + } + match &mut expr.kind { + ExprKind::Block(items) => items + .iter_mut() + .for_each(|e| retarget_probe(e, object, field)), + ExprKind::Function { body, .. } => retarget_probe(body, object, field), + _ => {} + } +} + +/// A field holding a fn pointer, called through `(h.f)(3)`. The callee is a +/// `.` FieldAccess (not a path), so Rust needs `(h.f)(3)`, not `h::f(3)`. +/// The AST is built from source with a `probe` callee retargeted to `h.f` +/// because the parser splits `(h.f)(3)` inside a block (FieldAccess span 0..0). +#[test] +fn test_g2b_t5_04_field_callee_is_not_a_path() { + let code = r#" +struct Holder { f: fn(i32) -> i32 } +fun double(x: i32) -> i32 { x * 2 } +fun apply(h: Holder) -> i32 { + probe(3) +} +fun main() { + let h = Holder { f: double } + println!("{}", apply(h)) +} +"#; + let mut ast = Parser::new(code).parse().expect("parses"); + retarget_probe(&mut ast, "h", "f"); + let rust = transpile(&ast); + assert!( + !rust.contains("h :: f"), + "field callee emitted as a path: {rust}" + ); + assert_eq!(compile_and_run(&rust, "field").trim(), "6", "{rust}"); +} + +/// The parser marks `a::b` (path) and leaves `a.b` (field) unmarked. +#[test] +fn test_g2b_t5_05_parser_marks_path_access() { + let path = Parser::new("helpers::double").parse().expect("parses"); + assert!(path.is_path_access(), "{path:?}"); + let field = Parser::new("helpers.double").parse().expect("parses"); + assert!(!field.is_path_access(), "{field:?}"); +} From a40d3c33867cf89804a82483c23306e013c773b1 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 21:25:47 +0200 Subject: [PATCH 013/106] G2B-T8: #[derive(Debug)] is accepted per the 5.0 attribute grammar test_bug_syntax_001 demanded an 'Attributes are not supported' error, the pre-5.0 behaviour that PARSER-ATTR-001 removed. ruchy-5.0-sovereign-platform.md section 3 defines attribute ::= '#[' IDENT ( '(' arg_list ')' )? ']', so the test now asserts -e accepts the struct and transpile carries #[derive(Debug..)] through. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 214 ++++++++++++++++----------------- tests/runtime_builtins_core.rs | 25 ++-- 2 files changed, 122 insertions(+), 117 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 21176bbbc..7441168e5 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:25.956192176Z", + "created_at": "2026-09-24T00:02:28.612303562Z", "git_context": null, "files": { "./build.rs": { @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 214, - 245, - 174, - 159, - 41, - 253, - 217, - 216, - 216, - 67, - 176, - 31, - 65, - 9, + 204, + 33, + 62, + 45, + 32, + 234, 7, - 124, - 36, - 215, - 108, - 93, - 37, - 106, - 229, - 23, - 158, - 25, - 146, + 201, + 223, + 162, + 166, + 30, 242, - 104, - 196, - 71, - 194 + 128, + 178, + 2, + 197, + 18, + 187, + 80, + 5, + 99, + 202, + 33, + 82, + 48, + 236, + 15, + 135, + 37, + 207, + 210 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.71066, + "duplication_ratio": 15.753769, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.71066, + "total": 75.75377, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3643,11 +3643,11 @@ }, { "source_metric": "Duplication", - "amount": 4.28934, + "amount": 4.246231, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 21.4%" + "issue": "Code duplication: 21.2%" }, { "source_metric": "StructuralComplexity", @@ -3655,7 +3655,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 67" + "issue": "High cognitive complexity: 68" }, { "source_metric": "StructuralComplexity", @@ -3663,7 +3663,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 63" + "issue": "High cyclomatic complexity: 64" } ], "critical_defects_count": 0, @@ -14637,48 +14637,48 @@ }, "./src/frontend/ast.rs": { "content_hash": [ - 76, - 135, - 195, - 247, - 86, - 171, - 33, - 130, - 153, - 29, - 244, - 186, - 207, - 108, - 53, - 138, - 223, - 196, + 82, + 27, + 236, + 8, + 96, + 198, + 228, + 181, 46, - 127, - 10, - 69, + 158, + 48, + 251, + 67, + 240, + 27, + 86, + 133, + 239, + 87, + 252, + 43, + 85, + 30, + 22, + 237, + 144, + 136, + 187, 147, - 157, - 228, - 229, - 127, - 57, - 96, - 191, - 135, - 243 + 97, + 195, + 202 ], "score": { "structural_complexity": 12.5, "semantic_complexity": 20.0, - "duplication_ratio": 15.49325, + "duplication_ratio": 15.500516, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 87.99325, + "total": 88.00052, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -14694,7 +14694,7 @@ }, { "source_metric": "Duplication", - "amount": 4.5067496, + "amount": 4.499484, "applied_to": [ "Duplication" ], @@ -18124,48 +18124,48 @@ }, "./src/frontend/parser/mod.rs": { "content_hash": [ - 247, - 213, - 22, - 110, - 214, - 62, - 87, - 16, - 46, - 183, - 244, - 147, - 183, - 65, - 240, - 241, - 6, - 13, - 86, - 106, - 247, + 116, + 45, + 232, + 201, + 218, + 233, + 237, + 227, + 182, + 70, 124, - 231, - 76, - 41, - 125, - 33, - 17, - 3, - 148, - 226, - 100 + 38, + 250, + 207, + 92, + 155, + 251, + 134, + 55, + 134, + 132, + 27, + 154, + 209, + 110, + 250, + 133, + 173, + 105, + 210, + 132, + 26 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.003122, + "duplication_ratio": 17.042398, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.00312, + "total": 77.0424, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -18177,15 +18177,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 68 duplicate code patterns" + "issue": "Found 67 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.9968784, + "amount": 2.957601, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.0%" + "issue": "Code duplication: 14.8%" }, { "source_metric": "StructuralComplexity", @@ -44864,7 +44864,7 @@ }, "summary": { "total_files": 528, - "avg_score": 90.33214, + "avg_score": 90.332306, "grade_distribution": { "A+": 195, "A": 176, diff --git a/tests/runtime_builtins_core.rs b/tests/runtime_builtins_core.rs index 9a9e396aa..520a41112 100644 --- a/tests/runtime_builtins_core.rs +++ b/tests/runtime_builtins_core.rs @@ -136,12 +136,16 @@ fun main() { } // ============================================================================ -// BUG #2: Cryptic error messages for unsupported Rust syntax -// Bug Report: ../ruchy-cookbook/RUCHY_ISSUE_SYNTAX_ERROR_REPORT.md +// BUG #2 (superseded): `#[derive(...)]` once failed with a cryptic +// "Unexpected token: AttributeStart", and this test then demanded an +// "Attributes are not supported" error. The 5.0 spec (ruchy-5.0-sovereign-platform.md +// section 3 grammar: attribute ::= '#[' IDENT ( '(' arg_list ')' )? ']') and +// PARSER-ATTR-001 (tests/parser_attribute_syntax.rs) make Rust-style attributes +// valid Ruchy, so the correct behaviour is: accepted, and carried to the Rust output. // ============================================================================ #[test] -fn test_bug_syntax_001_attribute_error_message() { +fn test_bug_syntax_001_rust_style_derive_attribute_accepted() { let code = r" #[derive(Debug)] struct Point { @@ -150,15 +154,16 @@ struct Point { } "; - let output = ruchy_cmd().arg("-e").arg(code).assert().failure(); + ruchy_cmd().arg("-e").arg(code).assert().success(); - // Should have helpful error message, not cryptic "Unexpected token: AttributeStart" - let stderr = String::from_utf8_lossy(&output.get_output().stderr); + let dir = tempfile::tempdir().expect("tempdir"); + let src = dir.path().join("derive.ruchy"); + std::fs::write(&src, code).expect("write source"); + let output = ruchy_cmd().arg("transpile").arg(&src).assert().success(); + let stdout = String::from_utf8_lossy(&output.get_output().stdout); assert!( - stderr.contains("Attributes are not supported") - || stderr.contains("does not support #[derive]") - || stderr.contains("Ruchy does not use Rust-style attributes"), - "Error message should explain that attributes are not supported. Got: {stderr}" + stdout.contains("#[derive(Debug") && stdout.contains("struct Point"), + "derive attribute must reach the Rust output. Got: {stdout}" ); } From 1eaed6cec3435d72d854abe6efe0131a1db86567 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 21:28:43 +0200 Subject: [PATCH 014/106] G2B-T9: a main that awaits runs on an in-program block_on, not async fn main async fun main transpiled to async fn main (rustc E0752) and a sync main using await emitted .await in a non-async fn (E0728). ruchy compile invokes rustc with no tokio, so main's body now runs as an async move block under a std-only, thread-parking __ruchy_block_on (no unsafe); a non-unit tail is bound and printed outside the println! macro so the awaited body stays rustfmt-formatted. Detection is exact: the emitted body contains the await keyword token. tests/g2b_t2b_misc.rs: sync-main-await, async-main, printed-tail, compiled and run. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2 +- src/backend/transpiler/function_transpiler.rs | 69 +++++++++++++++++++ tests/g2b_t2b_misc.rs | 49 +++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 tests/g2b_t2b_misc.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 7441168e5..e03eb267d 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:28.612303562Z", + "created_at": "2026-09-24T00:02:31.135213453Z", "git_context": null, "files": { "./build.rs": { diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index 5714f1417..5eabf86b9 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -95,6 +95,10 @@ impl Transpiler { // TRANSPILER-007: Clear current function return type after body transpilation self.current_function_return_type.replace(None); + // RHLGA-1: rustc rejects `async fn main` (E0752) and `.await` in a sync fn (E0728). + let (body_tokens, is_async) = + self.adapt_awaiting_main(name, body, body_tokens, is_async)?; + let return_type_tokens = if needs_lifetime { self.generate_return_type_tokens_with_lifetime(name, effective_return_type, body)? } else { @@ -192,6 +196,71 @@ impl Transpiler { } } +impl Transpiler { + /// A `main` that awaits gets a `block_on` body and a sync signature; others pass through. + /// Complexity: 2 + fn adapt_awaiting_main( + &self, + name: &str, + body: &Expr, + body_tokens: TokenStream, + is_async: bool, + ) -> Result<(TokenStream, bool)> { + if name == "main" && (is_async || Self::tokens_contain_await(&body_tokens)) { + Ok((self.generate_block_on_main_body(body)?, false)) + } else { + Ok((body_tokens, is_async)) + } + } + + /// True when the emitted Rust uses the `await` keyword (only legal in an async context). + /// Complexity: 3 + fn tokens_contain_await(tokens: &TokenStream) -> bool { + tokens.clone().into_iter().any(|tt| match tt { + proc_macro2::TokenTree::Ident(ident) => ident == "await", + proc_macro2::TokenTree::Group(group) => Self::tokens_contain_await(&group.stream()), + _ => false, + }) + } + + /// Body of a `main` that awaits: the original body runs as an `async move` block driven + /// by a thread-parking `block_on` (std only, no unsafe); a non-unit tail is printed. + /// Complexity: 2 + fn generate_block_on_main_body(&self, body: &Expr) -> Result { + let inner = self.generate_body_tokens(body, true)?; + let run = quote! { __ruchy_block_on(async move { #inner }) }; + let tail = if Self::has_non_unit_last_expr(body) { + quote! { let __ruchy_main_value = #run; println!("{:?}", __ruchy_main_value); } + } else { + quote! { #run; } + }; + Ok(quote! { + { + fn __ruchy_block_on(fut: F) -> F::Output { + struct ThreadWaker(::std::thread::Thread); + impl ::std::task::Wake for ThreadWaker { + fn wake(self: ::std::sync::Arc) { + self.0.unpark(); + } + } + let waker = ::std::task::Waker::from(::std::sync::Arc::new(ThreadWaker( + ::std::thread::current(), + ))); + let mut cx = ::std::task::Context::from_waker(&waker); + let mut fut = ::std::pin::pin!(fut); + loop { + if let ::std::task::Poll::Ready(value) = fut.as_mut().poll(&mut cx) { + return value; + } + ::std::thread::park(); + } + } + #tail + } + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/g2b_t2b_misc.rs b/tests/g2b_t2b_misc.rs new file mode 100644 index 000000000..71a888382 --- /dev/null +++ b/tests/g2b_t2b_misc.rs @@ -0,0 +1,49 @@ +#![allow(missing_docs)] +//! RHLGA-1 (G2b T2b): miscellaneous transpile/runtime defects, checked end to end. + +use assert_cmd::Command; + +fn ruchy_cmd() -> Command { + assert_cmd::cargo::cargo_bin_cmd!("ruchy") +} + +/// Compile `src` with `ruchy compile` and return the binary's stdout. +fn compile_and_run(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let source = dir.path().join("main.ruchy"); + let binary = dir.path().join("main_bin"); + std::fs::write(&source, src).expect("write source"); + ruchy_cmd() + .arg("compile") + .arg(&source) + .arg("-o") + .arg(&binary) + .assert() + .success(); + let out = std::process::Command::new(&binary) + .output() + .expect("run binary"); + assert!(out.status.success(), "binary failed: {out:?}"); + String::from_utf8_lossy(&out.stdout).into_owned() +} + +/// A sync `main` that awaits compiled to `.await` in a non-async fn (rustc E0728). +#[test] +fn test_rhlga_1_sync_main_awaiting_async_fn_compiles_and_runs() { + let src = "async fun fetch() -> i32 {\n 42\n}\n\nfun main() {\n let d = await fetch()\n println(d)\n}\n"; + assert_eq!(compile_and_run(src).trim(), "42"); +} + +/// `async fun main` compiled to `async fn main` (rustc E0752). +#[test] +fn test_rhlga_1_async_main_compiles_and_runs() { + let src = "async fun fetch() -> i32 {\n 40\n}\n\nasync fun main() {\n let d = await fetch()\n println(d + 2)\n}\n"; + assert_eq!(compile_and_run(src).trim(), "42"); +} + +/// A non-unit tail of an awaiting main is still printed (BOOK-COMPAT-015 semantics). +#[test] +fn test_rhlga_1_awaiting_main_prints_non_unit_tail() { + let src = "async fun fetch() -> i32 {\n 7\n}\n\nfun main() {\n let d = await fetch()\n d\n}\n"; + assert_eq!(compile_and_run(src).trim(), "7"); +} From c158a2e99baa16a41c8fe358051749c9736c3a47 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 21:34:25 +0200 Subject: [PATCH 015/106] G2B-S2: infra commands reject a missing path or a directory - infra plan/apply/drift/destroy require a regular file (verify_regular_file). `ruchy infra destroy /nonexistent` exited 0 because /nonexistent is a real directory on this host; it now exits 1 with "... is not a file". - CLI tests with a path missing on every host and with a directory. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 80 +++++++++---------- .../handlers/handlers_modules/sovereign.rs | 35 +++++++- tests/cli_contract_sovereign.rs | 26 ++++++ 3 files changed, 97 insertions(+), 44 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index e03eb267d..6240aadc9 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:31.135213453Z", + "created_at": "2026-09-24T00:02:33.493583007Z", "git_context": null, "files": { "./build.rs": { @@ -6194,48 +6194,48 @@ }, "./src/backend/transpiler/function_transpiler.rs": { "content_hash": [ - 176, - 103, - 190, - 94, - 78, - 249, - 64, - 137, - 21, + 35, + 243, 247, - 177, - 61, - 254, - 26, - 41, - 179, - 15, - 142, - 147, - 165, + 98, + 244, + 18, + 194, + 248, + 49, + 31, + 1, + 114, + 189, + 200, 68, - 57, - 254, - 41, - 42, - 253, - 4, - 78, - 112, - 40, - 40, - 157 + 228, + 147, + 129, + 16, + 150, + 100, + 145, + 229, + 38, + 247, + 47, + 224, + 189, + 234, + 71, + 99, + 120 ], "score": { - "structural_complexity": 19.6, + "structural_complexity": 18.7, "semantic_complexity": 20.0, - "duplication_ratio": 14.026667, + "duplication_ratio": 14.62963, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.62666, + "total": 93.329636, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -6247,23 +6247,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 22 duplicate code patterns" + "issue": "Found 29 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.9733334, + "amount": 5.37037, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.9%" + "issue": "Code duplication: 26.9%" }, { "source_metric": "StructuralComplexity", - "amount": 5.4, + "amount": 6.3, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 33" + "issue": "High cognitive complexity: 36" } ], "critical_defects_count": 0, @@ -44864,7 +44864,7 @@ }, "summary": { "total_files": 528, - "avg_score": 90.332306, + "avg_score": 90.33174, "grade_distribution": { "A+": 195, "A": 176, diff --git a/src/bin/handlers/handlers_modules/sovereign.rs b/src/bin/handlers/handlers_modules/sovereign.rs index c0d934319..8f59686cf 100644 --- a/src/bin/handlers/handlers_modules/sovereign.rs +++ b/src/bin/handlers/handlers_modules/sovereign.rs @@ -20,7 +20,7 @@ use ruchy::stdlib::simular_bridge::{SimConfig, SimResult}; /// Handle `ruchy infra plan `. pub fn handle_infra_plan(file: &Path) -> anyhow::Result<()> { - verify_file_exists(file, "infra plan")?; + verify_regular_file(file, "infra plan")?; println!("[ruchy infra plan] {}", file.display()); let plan = InfraPlan::empty(); println!(" {}", plan.summary()); @@ -32,7 +32,7 @@ pub fn handle_infra_plan(file: &Path) -> anyhow::Result<()> { /// Handle `ruchy infra apply `. pub fn handle_infra_apply(file: &Path, auto_approve: bool) -> anyhow::Result<()> { - verify_file_exists(file, "infra apply")?; + verify_regular_file(file, "infra apply")?; println!("[ruchy infra apply] {}", file.display()); let plan = InfraPlan::empty(); if !auto_approve && plan.has_changes() { @@ -48,7 +48,7 @@ pub fn handle_infra_apply(file: &Path, auto_approve: bool) -> anyhow::Result<()> /// Handle `ruchy infra drift `. pub fn handle_infra_drift(file: &Path) -> anyhow::Result<()> { - verify_file_exists(file, "infra drift")?; + verify_regular_file(file, "infra drift")?; println!("[ruchy infra drift] {}", file.display()); let state = InfraState::empty(); if state.resource_count() == 0 { @@ -69,7 +69,7 @@ pub fn handle_infra_status() -> anyhow::Result<()> { /// Handle `ruchy infra destroy `. pub fn handle_infra_destroy(file: &Path, auto_approve: bool) -> anyhow::Result<()> { - verify_file_exists(file, "infra destroy")?; + verify_regular_file(file, "infra destroy")?; println!("[ruchy infra destroy] {}", file.display()); let state = InfraState::empty(); if state.resource_count() == 0 { @@ -691,6 +691,16 @@ fn verify_file_exists(file: &Path, cmd: &str) -> anyhow::Result<()> { Ok(()) } +/// Infra specs are files: a directory (e.g. `/nonexistent` on hosts where it +/// exists) must be an error, not a silent no-op (G2B-S2). +fn verify_regular_file(file: &Path, cmd: &str) -> anyhow::Result<()> { + verify_file_exists(file, cmd)?; + if !file.is_file() { + anyhow::bail!("[ruchy {cmd}] {} is not a file", file.display()); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -746,6 +756,23 @@ mod tests { assert!(handle_infra_destroy(f.path(), true).is_ok()); } + /// G2B-S2: `ruchy infra destroy /nonexistent` exited 0 on hosts where + /// `/nonexistent` is a directory; an infra spec must be a regular file. + #[test] + fn test_infra_destroy_missing_file_is_error() { + assert!(handle_infra_destroy(&missing_path(), true).is_err()); + } + + #[test] + fn test_infra_commands_reject_a_directory() { + let dir = tempfile::tempdir().expect("tempdir"); + let err = handle_infra_destroy(dir.path(), true).expect_err("directory is not a spec"); + assert!(err.to_string().contains("not a file"), "got: {err}"); + assert!(handle_infra_plan(dir.path()).is_err()); + assert!(handle_infra_apply(dir.path(), true).is_err()); + assert!(handle_infra_drift(dir.path()).is_err()); + } + #[test] fn test_sim_run_with_seed() { let f = temp_file(); diff --git a/tests/cli_contract_sovereign.rs b/tests/cli_contract_sovereign.rs index 825040ed5..f1f5766df 100644 --- a/tests/cli_contract_sovereign.rs +++ b/tests/cli_contract_sovereign.rs @@ -91,6 +91,32 @@ fn test_infra_destroy_missing_file() { .failure(); } +/// G2B-S2: a path that is missing on every host (`/nonexistent` is a real +/// directory on some machines, so the test above also covers "not a file"). +#[test] +fn test_infra_destroy_path_missing_everywhere() { + let dir = TempDir::new().unwrap(); + ruchy_cmd() + .arg("infra") + .arg("destroy") + .arg(dir.path().join("absent.ruchy")) + .assert() + .failure() + .stderr(predicate::str::contains("File not found")); +} + +#[test] +fn test_infra_destroy_directory_is_not_a_spec() { + let dir = TempDir::new().unwrap(); + ruchy_cmd() + .arg("infra") + .arg("destroy") + .arg(dir.path()) + .assert() + .failure() + .stderr(predicate::str::contains("not a file")); +} + // ============================================================================ // ruchy sim // ============================================================================ From bb85e1847f6788c9369e2822dc0e2fb134f7c2a3 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 21:46:10 +0200 Subject: [PATCH 016/106] G2B-S2: release_hygiene names a dirty tree; pmat-written paths gitignored - cargo package --list refuses any dirty tree; the test now fails with "working tree is dirty: ..." plus the git status lines when that is the cause, instead of only cargo's raw error. No --allow-dirty. - Root .gitignore covers src/.pmat/, .pmat/*.db, .pmat/jidoka.jsonl, .pmat/clippy-receipt, .pmat/dead-code-cache-*.json (previously ignored only by pmat's own untracked nested .gitignore, absent in a fresh clone); a test checks them against the root .gitignore in a scratch repo. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 9 ++++ .pmat/baseline.json | 84 +++++++++++++++---------------- tests/release_hygiene.rs | 104 +++++++++++++++++++++++++++++++-------- 3 files changed, 135 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index c78dc78fb..a2abb681d 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,12 @@ dependency_cleanup_analysis.md # PMAT-136: dogfood receipts (written by the ruchy-dogfood skill) .dogfood/ + +# G2B-S2: files pmat writes while tests and hooks run. pmat also drops a `*` +# .gitignore inside each .pmat/ it creates, but that file is untracked and absent +# in a fresh clone; without these rules `cargo package` sees a dirty tree. +src/.pmat/ +.pmat/*.db +.pmat/jidoka.jsonl +.pmat/clippy-receipt +.pmat/dead-code-cache-*.json diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 6240aadc9..70d073006 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:33.493583007Z", + "created_at": "2026-09-24T00:02:35.789400698Z", "git_context": null, "files": { "./build.rs": { @@ -11727,49 +11727,49 @@ }, "./src/bin/handlers/handlers_modules/sovereign.rs": { "content_hash": [ - 112, - 104, - 249, - 213, - 99, - 193, - 94, - 172, - 199, - 27, - 118, + 244, + 107, + 68, + 82, + 187, + 169, + 253, + 106, + 175, + 41, + 96, + 42, + 49, + 46, + 11, + 187, + 91, 25, - 131, - 75, - 71, - 178, - 199, - 134, - 59, - 57, - 93, - 190, + 47, + 160, + 225, + 41, + 189, + 91, + 61, + 161, + 63, + 193, 84, - 212, - 50, - 235, - 85, - 48, - 3, - 133, + 206, 70, - 42 + 86 ], "score": { - "structural_complexity": 3.0, + "structural_complexity": 2.5, "semantic_complexity": 20.0, - "duplication_ratio": 17.069923, + "duplication_ratio": 17.13355, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 80.06992, - "grade": "B+", + "total": 79.63355, + "grade": "B", "confidence": 1.0, "language": "Rust", "file_path": "./src/bin/handlers/handlers_modules/sovereign.rs", @@ -11784,11 +11784,11 @@ }, { "source_metric": "Duplication", - "amount": 2.9300776, + "amount": 2.8664496, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 14.7%" + "issue": "Code duplication: 14.3%" }, { "source_metric": "StructuralComplexity", @@ -11796,15 +11796,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 62" + "issue": "High cognitive complexity: 63" }, { "source_metric": "StructuralComplexity", - "amount": 12.0, + "amount": 12.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 54" + "issue": "High cyclomatic complexity: 55" } ], "critical_defects_count": 0, @@ -44864,13 +44864,13 @@ }, "summary": { "total_files": 528, - "avg_score": 90.33174, + "avg_score": 90.33092, "grade_distribution": { "A+": 195, "A": 176, "A-": 31, - "B+": 30, - "B": 67, + "B+": 29, + "B": 68, "B-": 26, "C": 1, "F": 2 diff --git a/tests/release_hygiene.rs b/tests/release_hygiene.rs index 3f71b7e23..ea5ff5d0d 100644 --- a/tests/release_hygiene.rs +++ b/tests/release_hygiene.rs @@ -21,6 +21,88 @@ fn git_ls_files() -> Vec { .collect() } +/// `git status --porcelain` lines (tracked changes and untracked, non-ignored files). +fn dirty_paths() -> Vec { + let output = Command::new("git") + .args(["status", "--porcelain"]) + .output() + .expect("failed to run git status"); + String::from_utf8_lossy(&output.stdout) + .lines() + .map(std::string::ToString::to_string) + .collect() +} + +/// The failure message for a failed `cargo package --list`. `cargo package` +/// refuses any dirty tree, so a dirty tree is named as the cause explicitly +/// instead of surfacing only cargo's raw error (G2B-S2). `--allow-dirty` is +/// deliberately not used: the check is about what a clean release would ship. +fn package_failure_message(dirty: &[String], cargo_stderr: &str) -> String { + if dirty.is_empty() { + return format!("cargo package --list failed: {cargo_stderr}"); + } + format!( + "working tree is dirty: cargo package refuses to run until these are committed \ + or ignored:\n{}\ncargo said: {cargo_stderr}", + dirty.join("\n") + ) +} + +/// `cargo package --list -p ruchy` stdout; fails naming a dirty tree when that is the cause. +fn cargo_package_list() -> String { + let output = Command::new(env!("CARGO")) + .args(["package", "--list", "-p", "ruchy"]) + .output() + .expect("failed to run cargo package --list"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "{}", + package_failure_message(&dirty_paths(), &stderr) + ); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +#[test] +fn test_g2b_s2_package_failure_names_a_dirty_tree() { + let msg = package_failure_message(&["?? src/.pmat/tdg-cold.db".to_string()], "error: x"); + assert!(msg.starts_with("working tree is dirty"), "{msg}"); + assert!(msg.contains("src/.pmat/tdg-cold.db"), "{msg}"); + assert!(msg.contains("error: x"), "{msg}"); + let clean = package_failure_message(&[], "error: y"); + assert!(!clean.contains("dirty"), "{clean}"); +} + +/// Paths pmat writes while tests and hooks run must be ignored by the ROOT +/// `.gitignore`, or `cargo package` sees a dirty tree (G2B-S2). pmat also drops +/// a `*` `.gitignore` inside each `.pmat/` it creates, but that file is itself +/// untracked and absent in a fresh clone, so it is checked in isolation here: a +/// scratch repository holding only the root `.gitignore`. +#[test] +fn test_g2b_s2_tool_written_paths_are_ignored() { + let scratch = tempfile::TempDir::new().expect("tempdir"); + std::fs::copy(".gitignore", scratch.path().join(".gitignore")).expect("copy .gitignore"); + let git = |args: &[&str]| { + Command::new("git") + .current_dir(scratch.path()) + .args(args) + .status() + .expect("failed to run git") + }; + assert!(git(&["init", "-q"]).success(), "git init failed"); + for path in [ + "src/.pmat/tdg-cold.db", + "src/.pmat/tdg-warm.db", + ".pmat/tdg-cold.db", + ".pmat/jidoka.jsonl", + ".pmat/clippy-receipt", + ".pmat/dead-code-cache-eb-d8.json", + ] { + let ignored = git(&["check-ignore", "--no-index", "-q", path]).success(); + assert!(ignored, "{path} must be ignored by the root .gitignore"); + } +} + #[test] fn test_pmat_092_git_tracked_paths_no_colon() { let files = git_ls_files(); @@ -105,16 +187,7 @@ fn test_pmat_092_pmat_work_colon_dirs_not_tracked() { #[test] fn test_pmat_092_pmat_work_not_in_package_list() { - let output = Command::new(env!("CARGO")) - .args(["package", "--list", "-p", "ruchy"]) - .output() - .expect("failed to run cargo package --list"); - assert!( - output.status.success(), - "cargo package --list failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - let files = String::from_utf8_lossy(&output.stdout); + let files = cargo_package_list(); let offenders: Vec<&str> = files .lines() .filter(|l| l.starts_with(".pmat-work/")) @@ -130,16 +203,7 @@ fn test_pmat_092_pmat_work_not_in_package_list() { /// true so `cargo install ruchy` remains reproducible. #[test] fn test_pmat_092_cargo_lock_present_in_package_list() { - let output = Command::new(env!("CARGO")) - .args(["package", "--list", "-p", "ruchy"]) - .output() - .expect("failed to run cargo package --list"); - assert!( - output.status.success(), - "cargo package --list failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - let files = String::from_utf8_lossy(&output.stdout); + let files = cargo_package_list(); assert!( files.lines().any(|l| l == "Cargo.lock"), "Cargo.lock must be present in the packaged file list" From de3e1a0d05e7c32c7b49eef7c46fd8d8c078bfc8 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 21:54:01 +0200 Subject: [PATCH 017/106] G2B-S2: serve --watch keeps serving when the file watcher cannot be created Root cause: FileWatcher::new failed with EMFILE ("Too many open files") when the per-user inotify instance limit (fs.inotify.max_user_instances=128 here) was exhausted by other processes, and run_watch_mode propagated it with `?`, so the server exited ~immediately (reproduced: 60 extra inotify holders -> exit 1). - open_watcher warns on stderr (cause + remedy) and serve falls back to run_normal_mode; after the fix the same reproduction keeps serving. - The watcher is created once per session, not once per restart. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2 +- src/bin/handlers/serve_handler.rs | 64 +++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 70d073006..5d958b044 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:35.789400698Z", + "created_at": "2026-09-24T00:02:37.571491712Z", "git_context": null, "files": { "./build.rs": { diff --git a/src/bin/handlers/serve_handler.rs b/src/bin/handlers/serve_handler.rs index edd8202d8..22743466b 100644 --- a/src/bin/handlers/serve_handler.rs +++ b/src/bin/handlers/serve_handler.rs @@ -219,10 +219,21 @@ fn run_watch_mode( num_cpus: usize, #[cfg(unix)] shutdown_rx: &std::sync::mpsc::Receiver<()>, ) -> Result<()> { + // One watcher for the whole session: re-creating it on every restart spent + // an inotify instance each time, and a failure to get one killed the server. + let Some(mut watcher) = open_watcher(directory, debounce) else { + return run_normal_mode( + runtime, + app.clone(), + host, + port, + verbose, + num_cpus, + #[cfg(unix)] + shutdown_rx, + ); + }; loop { - let mut watcher = - ruchy::server::watcher::FileWatcher::new(vec![directory.to_path_buf()], debounce)?; - let addr = format!("{}:{}", host, port); let app_clone = app.clone(); let server_handle = runtime.spawn(async move { @@ -257,6 +268,34 @@ fn run_watch_mode( } } +/// Open the file watcher for `directory`, or `None` (with a warning on stderr) +/// when the host cannot watch it. +/// +/// `--watch` used to exit the server when the watcher could not be created. On a +/// busy host the per-user inotify instance limit (`fs.inotify.max_user_instances`, +/// 128 by default) is exhausted and creation fails with EMFILE ("Too many open +/// files"); the server then keeps serving without reload instead (G2B-S2). +#[cfg(feature = "notebook")] +fn open_watcher(directory: &Path, debounce: u64) -> Option { + match ruchy::server::watcher::FileWatcher::new(vec![directory.to_path_buf()], debounce) { + Ok(watcher) => Some(watcher), + Err(err) => { + eprintln!("{}", watch_unavailable_message(directory, &err)); + None + } + } +} + +/// Warning printed when `--watch` cannot watch `directory`. +#[cfg(feature = "notebook")] +fn watch_unavailable_message(directory: &Path, err: &dyn std::fmt::Display) -> String { + format!( + " warning: --watch disabled, cannot watch {}: {err}; serving without reload \ + (if this is \"Too many open files\", raise fs.inotify.max_user_instances)", + directory.display() + ) +} + /// Handle file changes in watch mode /// Complexity: 5 (Toyota Way: <10) #[cfg(feature = "notebook")] @@ -412,6 +451,25 @@ mod tests { use super::*; use tempfile::TempDir; + /// G2B-S2: a watcher that cannot be created must not end the server. + #[test] + #[cfg(feature = "notebook")] + fn test_open_watcher_on_unwatchable_path_degrades_to_none() { + let dir = tempfile::tempdir().expect("tempdir"); + let missing = dir.path().join("absent"); + assert!(open_watcher(&missing, 100).is_none()); + } + + #[test] + #[cfg(feature = "notebook")] + fn test_watch_unavailable_message_names_the_cause_and_remedy() { + let msg = watch_unavailable_message(Path::new("site"), &"Too many open files"); + assert!(msg.contains("--watch disabled"), "{msg}"); + assert!(msg.contains("site"), "{msg}"); + assert!(msg.contains("Too many open files"), "{msg}"); + assert!(msg.contains("max_user_instances"), "{msg}"); + } + #[test] fn test_serve_handler_stub() { // Serve handler tests require the notebook feature From eb7e2b82385afe3761c9abd6fd38ae5959c21679 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 22:31:07 +0200 Subject: [PATCH 018/106] G2BS6: serve_handler tests stop through a shutdown seam instead of serving forever - handle_serve_command = ServeRequest + serve_until(request, shutdown_on_signal) - serve_until takes the shutdown channel installer; tests pass a pre-sent channel - run_normal_mode returns the server's own error (e.g. port in use) instead of waiting forever - new tests: real GET then shutdown, port-in-use error, watch mode stops - pid-file test uses a tempdir, not /tmp/pid (PidFile::new kills the recorded pid) - serve_handler tests under --features notebook: 17 passed in 0.15s (was: hung >60s) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 80 ++--- src/bin/handlers/serve_handler.rs | 473 ++++++++++++++++++++++-------- 2 files changed, 390 insertions(+), 163 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 5d958b044..dccfef4c9 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:37.571491712Z", + "created_at": "2026-09-24T00:02:39.200021013Z", "git_context": null, "files": { "./build.rs": { @@ -13055,48 +13055,48 @@ }, "./src/bin/handlers/serve_handler.rs": { "content_hash": [ - 122, - 31, - 120, - 31, + 245, + 221, + 133, 188, - 196, - 242, - 198, - 38, - 254, - 99, - 202, - 61, - 111, - 170, - 134, - 146, - 196, - 190, - 64, - 105, - 209, - 210, - 237, - 186, - 73, + 19, + 172, + 180, + 148, + 96, + 249, + 169, + 84, + 165, + 95, + 125, + 160, + 211, + 149, + 59, + 238, + 204, 79, - 168, - 188, - 98, - 181, - 167 + 69, + 157, + 114, + 144, + 74, + 106, + 38, + 173, + 115, + 88 ], "score": { - "structural_complexity": 19.9, + "structural_complexity": 19.6, "semantic_complexity": 17.5, - "duplication_ratio": 16.029411, + "duplication_ratio": 16.122448, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.42941, + "total": 93.22244, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -13108,23 +13108,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 72 duplicate code patterns" + "issue": "Found 73 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.9705882, + "amount": 3.877551, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 19.9%" + "issue": "Code duplication: 19.4%" }, { "source_metric": "StructuralComplexity", - "amount": 5.1000004, + "amount": 5.4, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 32" + "issue": "High cognitive complexity: 33" }, { "source_metric": "SemanticComplexity", @@ -44864,7 +44864,7 @@ }, "summary": { "total_files": 528, - "avg_score": 90.33092, + "avg_score": 90.33052, "grade_distribution": { "A+": 195, "A": 176, diff --git a/src/bin/handlers/serve_handler.rs b/src/bin/handlers/serve_handler.rs index 22743466b..3744b17f1 100644 --- a/src/bin/handlers/serve_handler.rs +++ b/src/bin/handlers/serve_handler.rs @@ -47,21 +47,104 @@ pub fn handle_serve_command( pid_file: Option<&Path>, watch_wasm: bool, ) -> Result<()> { - use axum::{http::HeaderValue, Router}; - use tower::ServiceBuilder; - use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer}; + let request = ServeRequest { + directory, + port, + host, + verbose, + watch, + debounce, + pid_file, + watch_wasm, + }; + serve_until(&request, shutdown_on_signal) +} - validate_serve_directory(directory)?; +/// The arguments of one `ruchy serve` invocation. +#[cfg(feature = "notebook")] +struct ServeRequest<'a> { + directory: &'a Path, + port: u16, + host: &'a str, + verbose: bool, + watch: bool, + debounce: u64, + pid_file: Option<&'a Path>, + watch_wasm: bool, +} + +/// Serve `request` until the channel returned by `install_shutdown` receives. +/// +/// `install_shutdown` runs once the server is set up. The CLI passes +/// [`shutdown_on_signal`] (Ctrl+C / SIGTERM); tests pass a channel they +/// control, so they drive the whole serve path without serving forever. +/// Complexity: 4 (Toyota Way: <10) +#[cfg(feature = "notebook")] +fn serve_until( + request: &ServeRequest<'_>, + install_shutdown: impl FnOnce() -> std::sync::mpsc::Receiver<()>, +) -> Result<()> { + validate_serve_directory(request.directory)?; // Initialize PID file if requested - let _pid_guard = if let Some(pid_path) = pid_file { - Some(ruchy::server::PidFile::create(pid_path)?) - } else { - None - }; + let _pid_guard = request + .pid_file + .map(ruchy::server::PidFile::create) + .transpose()?; // World-class UX: Colored startup banner (vite-style) - print_startup_banner(host, port, directory, watch, watch_wasm); + print_startup_banner( + request.host, + request.port, + request.directory, + request.watch, + request.watch_wasm, + ); + let app = build_static_app(request.directory); + + // PERFORMANCE: Create optimized tokio runtime (multi-threaded, CPU-bound) + let num_cpus = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1); + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(num_cpus) + .enable_all() + .build()?; + + let shutdown_rx = install_shutdown(); + if request.watch { + run_watch_mode( + &runtime, + &app, + request.directory, + request.host, + request.port, + request.verbose, + request.debounce, + request.watch_wasm, + num_cpus, + &shutdown_rx, + ) + } else { + run_normal_mode( + &runtime, + app, + request.host, + request.port, + request.verbose, + num_cpus, + &shutdown_rx, + ) + } +} + +/// Static file service for `directory` with the WASM isolation headers. +/// Complexity: 1 (Toyota Way: <10) +#[cfg(feature = "notebook")] +fn build_static_app(directory: &Path) -> axum::Router { + use axum::{http::HeaderValue, Router}; + use tower::ServiceBuilder; + use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer}; // Build the Axum app with static file serving + WASM headers let serve_dir = ServeDir::new(directory) @@ -71,7 +154,7 @@ pub fn handle_serve_command( // Add WASM-specific headers for SharedArrayBuffer support (HTTP-003) // Required for: WebAssembly threading, SharedArrayBuffer, Atomics // Reference: https://web.dev/coop-coep/ - let app = Router::new().fallback_service(serve_dir).layer( + Router::new().fallback_service(serve_dir).layer( ServiceBuilder::new() // Cross-Origin-Opener-Policy: Isolate browsing context .layer(SetResponseHeaderLayer::if_not_present( @@ -83,19 +166,16 @@ pub fn handle_serve_command( axum::http::header::HeaderName::from_static("cross-origin-embedder-policy"), HeaderValue::from_static("require-corp"), )), - ); - - // PERFORMANCE: Create optimized tokio runtime (multi-threaded, CPU-bound) - let num_cpus = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(1); - let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(num_cpus) - .enable_all() - .build()?; + ) +} - // Setup signal handling for graceful shutdown (Ctrl+C) - #[cfg(unix)] +/// A channel that receives once the process gets SIGINT or SIGTERM (Ctrl+C). +/// +/// On non-Unix targets no handler is installed: the channel never receives +/// and Ctrl+C ends the process through the default handler. +/// Complexity: 2 (Toyota Way: <10) +#[cfg(feature = "notebook")] +fn shutdown_on_signal() -> std::sync::mpsc::Receiver<()> { let (shutdown_tx, shutdown_rx) = std::sync::mpsc::channel::<()>(); #[cfg(unix)] @@ -103,43 +183,18 @@ pub fn handle_serve_command( use signal_hook::consts::{SIGINT, SIGTERM}; use signal_hook::iterator::Signals; - let shutdown_tx_clone = shutdown_tx; std::thread::spawn(move || { let mut signals = Signals::new([SIGINT, SIGTERM]).expect("Failed to register signal handlers"); if let Some(_sig) = signals.forever().next() { - let _ = shutdown_tx_clone.send(()); + let _ = shutdown_tx.send(()); } }); } + #[cfg(not(unix))] + drop(shutdown_tx); - #[allow(unreachable_code)] // Watch mode and Unix signal handling both return early - if watch { - run_watch_mode( - &runtime, - &app, - directory, - host, - port, - verbose, - debounce, - watch_wasm, - num_cpus, - #[cfg(unix)] - &shutdown_rx, - ) - } else { - run_normal_mode( - &runtime, - app, - host, - port, - verbose, - num_cpus, - #[cfg(unix)] - &shutdown_rx, - ) - } + shutdown_rx } /// Print colored startup banner (vite-style) @@ -217,7 +272,7 @@ fn run_watch_mode( debounce: u64, watch_wasm: bool, num_cpus: usize, - #[cfg(unix)] shutdown_rx: &std::sync::mpsc::Receiver<()>, + shutdown_rx: &std::sync::mpsc::Receiver<()>, ) -> Result<()> { // One watcher for the whole session: re-creating it on every restart spent // an inotify instance each time, and a failure to get one killed the server. @@ -229,7 +284,6 @@ fn run_watch_mode( port, verbose, num_cpus, - #[cfg(unix)] shutdown_rx, ); }; @@ -249,7 +303,6 @@ fn run_watch_mode( // Poll for file changes AND shutdown signal loop { // Check for shutdown signal - #[cfg(unix)] if shutdown_rx.try_recv().is_ok() { print_shutdown_message(); server_handle.abort(); @@ -371,8 +424,11 @@ fn print_restart_message() { } } -/// Run server in normal mode (no watch) -/// Complexity: 6 (Toyota Way: <10) +/// Run server in normal mode (no watch) until shutdown is requested. +/// +/// A server that ends on its own (e.g. the address cannot be bound) returns +/// its error instead of leaving the process waiting on a dead server. +/// Complexity: 4 (Toyota Way: <10) #[cfg(feature = "notebook")] fn run_normal_mode( runtime: &tokio::runtime::Runtime, @@ -381,41 +437,10 @@ fn run_normal_mode( port: u16, verbose: bool, num_cpus: usize, - #[cfg(unix)] shutdown_rx: &std::sync::mpsc::Receiver<()>, + shutdown_rx: &std::sync::mpsc::Receiver<()>, ) -> Result<()> { let addr = format!("{}:{}", host, port); - - #[cfg(unix)] - { - let addr_clone = addr; - let verbose_clone = verbose; - let num_cpus_clone = num_cpus; - let server_future = async move { - let listener = tokio::net::TcpListener::bind(&addr_clone).await?; - - if verbose_clone { - println!("✅ Server started ({} workers)", num_cpus_clone); - } - - axum::serve(listener, app).await - }; - - // Spawn server task - let server_handle = runtime.spawn(server_future); - - // Wait for shutdown signal - loop { - if shutdown_rx.try_recv().is_ok() { - print_shutdown_message(); - server_handle.abort(); - return Ok(()); - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - } - - #[cfg(not(unix))] - runtime.block_on(async { + let server_handle = runtime.spawn(async move { let listener = tokio::net::TcpListener::bind(&addr).await?; if verbose { @@ -423,10 +448,50 @@ fn run_normal_mode( } axum::serve(listener, app).await - })?; + }); - #[allow(unreachable_code)] - Ok(()) + wait_for_shutdown_or_exit(runtime, server_handle, shutdown_rx) +} + +/// Block until shutdown is requested (abort the server, `Ok`) or the server +/// task ends on its own (return its result). +/// Complexity: 2 (Toyota Way: <10) +#[cfg(feature = "notebook")] +fn wait_for_shutdown_or_exit( + runtime: &tokio::runtime::Runtime, + mut server_handle: tokio::task::JoinHandle>, + shutdown_rx: &std::sync::mpsc::Receiver<()>, +) -> Result<()> { + loop { + if let Some(outcome) = poll_server(runtime, &mut server_handle, shutdown_rx) { + return outcome; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} + +/// One poll: `Some` once shutdown was requested or the server task ended. +/// Complexity: 3 (Toyota Way: <10) +#[cfg(feature = "notebook")] +fn poll_server( + runtime: &tokio::runtime::Runtime, + server_handle: &mut tokio::task::JoinHandle>, + shutdown_rx: &std::sync::mpsc::Receiver<()>, +) -> Option> { + if shutdown_rx.try_recv().is_ok() { + print_shutdown_message(); + server_handle.abort(); + return Some(Ok(())); + } + if !server_handle.is_finished() { + return None; + } + let joined = runtime.block_on(server_handle); + Some( + joined + .map_err(anyhow::Error::from) + .and_then(|served| Ok(served?)), + ) } #[cfg(not(feature = "notebook"))] @@ -470,10 +535,162 @@ mod tests { assert!(msg.contains("max_user_instances"), "{msg}"); } + /// A channel that has already received the shutdown request. + #[cfg(feature = "notebook")] + fn stopped() -> std::sync::mpsc::Receiver<()> { + let (tx, rx) = std::sync::mpsc::channel(); + tx.send(()).expect("send shutdown"); + rx + } + + /// Drive the serve path for these arguments without serving forever. + /// + /// With the notebook feature the server is set up (PID file, banner, app, + /// runtime, listener task, watcher) and stopped through the shutdown seam; + /// without it the stub is called directly. + fn serve_then_stop( + directory: &Path, + port: u16, + host: &str, + verbose: bool, + watch: bool, + debounce: u64, + pid_file: Option<&Path>, + watch_wasm: bool, + ) -> Result<()> { + #[cfg(feature = "notebook")] + { + let request = ServeRequest { + directory, + port, + host, + verbose, + watch, + debounce, + pid_file, + watch_wasm, + }; + serve_until(&request, stopped) + } + #[cfg(not(feature = "notebook"))] + handle_serve_command( + directory, port, host, verbose, watch, debounce, pid_file, watch_wasm, + ) + } + + /// A valid serve request shuts down cleanly with the notebook feature and + /// names the missing feature without it. + fn assert_served(result: Result<()>) { + #[cfg(feature = "notebook")] + assert!(result.is_ok(), "{result:?}"); + #[cfg(not(feature = "notebook"))] + assert!(result + .expect_err("stub must refuse") + .to_string() + .contains("notebook feature"),); + } + + /// A free local port (bound, then released). + #[cfg(feature = "notebook")] + fn free_port() -> u16 { + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind port 0"); + listener.local_addr().expect("local addr").port() + } + + /// GET `path` from 127.0.0.1:`port`, retrying until the server listens. + #[cfg(feature = "notebook")] + fn http_get(port: u16, path: &str) -> String { + use std::io::{Read, Write}; + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + loop { + if let Ok(mut stream) = std::net::TcpStream::connect(("127.0.0.1", port)) { + let request = + format!("GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n"); + stream.write_all(request.as_bytes()).expect("write request"); + let mut response = String::new(); + stream.read_to_string(&mut response).expect("read response"); + return response; + } + assert!( + std::time::Instant::now() < deadline, + "server never listened" + ); + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + + /// RHLGA-1: the server really serves the directory, then stops when the + /// shutdown channel receives, and the call returns. + #[test] + #[cfg(feature = "notebook")] + fn test_serve_until_serves_files_then_stops_on_shutdown() { + let temp_dir = TempDir::new().unwrap(); + std::fs::write(temp_dir.path().join("hello.txt"), "hello from serve").unwrap(); + let port = free_port(); + let (tx, rx) = std::sync::mpsc::channel(); + let client = std::thread::spawn(move || { + let response = http_get(port, "/hello.txt"); + tx.send(()).expect("send shutdown"); + response + }); + let request = ServeRequest { + directory: temp_dir.path(), + port, + host: "127.0.0.1", + verbose: false, + watch: false, + debounce: 100, + pid_file: None, + watch_wasm: false, + }; + serve_until(&request, move || rx).expect("serve until shutdown"); + let response = client.join().expect("client thread"); + assert!(response.starts_with("HTTP/1.1 200"), "{response}"); + assert!(response.contains("hello from serve"), "{response}"); + assert!( + response.contains("cross-origin-opener-policy: same-origin"), + "{response}" + ); + } + + /// RHLGA-1: an address that cannot be bound is reported, not waited on. + #[test] + #[cfg(feature = "notebook")] + fn test_serve_until_reports_a_port_in_use() { + let temp_dir = TempDir::new().unwrap(); + let taken = std::net::TcpListener::bind("127.0.0.1:0").expect("bind port 0"); + let port = taken.local_addr().expect("local addr").port(); + let (_tx, rx) = std::sync::mpsc::channel::<()>(); + let request = ServeRequest { + directory: temp_dir.path(), + port, + host: "127.0.0.1", + verbose: false, + watch: false, + debounce: 100, + pid_file: None, + watch_wasm: false, + }; + let err = serve_until(&request, move || rx).expect_err("port is taken"); + assert!(err.to_string().to_lowercase().contains("in use"), "{err}"); + } + + /// RHLGA-1: watch mode also stops when the shutdown channel receives. #[test] - fn test_serve_handler_stub() { - // Serve handler tests require the notebook feature - // This is a placeholder + #[cfg(feature = "notebook")] + fn test_serve_until_watch_mode_stops_on_shutdown() { + let temp_dir = TempDir::new().unwrap(); + let request = ServeRequest { + directory: temp_dir.path(), + port: free_port(), + host: "127.0.0.1", + verbose: true, + watch: true, + debounce: 50, + pid_file: None, + watch_wasm: false, + }; + serve_until(&request, stopped).expect("watch mode stops"); } // ===== EXTREME TDD Round 145 - Serve Handler Tests ===== @@ -548,22 +765,23 @@ mod tests { #[test] fn test_serve_command_default_parameters() { // Verify function signature accepts expected parameters - let _ = handle_serve_command( - Path::new("."), + let temp_dir = TempDir::new().unwrap(); + assert_served(serve_then_stop( + temp_dir.path(), 3000, "0.0.0.0", true, true, 1000, - Some(Path::new("/tmp/pid")), + Some(&temp_dir.path().join("server.pid")), true, - ); + )); } #[test] fn test_serve_command_with_verbose() { let temp_dir = TempDir::new().unwrap(); - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 8081, "127.0.0.1", @@ -572,14 +790,14 @@ mod tests { 500, None, false, - ); + )); } #[test] fn test_serve_command_various_hosts() { let temp_dir = TempDir::new().unwrap(); // Test localhost - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 8082, "localhost", @@ -588,9 +806,9 @@ mod tests { 100, None, false, - ); + )); // Test 0.0.0.0 - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 8083, "0.0.0.0", @@ -599,13 +817,13 @@ mod tests { 100, None, false, - ); + )); } #[test] fn test_serve_command_various_ports() { let temp_dir = TempDir::new().unwrap(); - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 80, "127.0.0.1", @@ -614,8 +832,8 @@ mod tests { 100, None, false, - ); - let _ = handle_serve_command( + )); + assert_served(serve_then_stop( temp_dir.path(), 443, "127.0.0.1", @@ -624,8 +842,8 @@ mod tests { 100, None, false, - ); - let _ = handle_serve_command( + )); + assert_served(serve_then_stop( temp_dir.path(), 65535, "127.0.0.1", @@ -634,14 +852,14 @@ mod tests { 100, None, false, - ); + )); } #[test] fn test_serve_command_debounce_values() { let temp_dir = TempDir::new().unwrap(); // Minimum debounce - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 8084, "127.0.0.1", @@ -650,9 +868,9 @@ mod tests { 1, // 1ms None, false, - ); + )); // Large debounce - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 8085, "127.0.0.1", @@ -661,13 +879,13 @@ mod tests { 10000, // 10s None, false, - ); + )); } #[test] fn test_serve_command_with_wasm_watch() { let temp_dir = TempDir::new().unwrap(); - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 8086, "127.0.0.1", @@ -676,7 +894,7 @@ mod tests { 500, None, true, // watch_wasm - ); + )); } // ===== EXTREME TDD Round 153 - Serve Handler Tests ===== @@ -685,7 +903,7 @@ mod tests { fn test_serve_command_all_flags() { let temp_dir = TempDir::new().unwrap(); let pid_path = temp_dir.path().join("server.pid"); - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 8087, "127.0.0.1", @@ -694,13 +912,13 @@ mod tests { 250, // debounce Some(&pid_path), true, // watch_wasm - ); + )); } #[test] fn test_serve_command_zero_debounce() { let temp_dir = TempDir::new().unwrap(); - let _ = handle_serve_command( + assert_served(serve_then_stop( temp_dir.path(), 8088, "127.0.0.1", @@ -709,7 +927,7 @@ mod tests { 0, // zero debounce None, false, - ); + )); } #[test] @@ -717,6 +935,15 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let nested = temp_dir.path().join("a").join("b").join("c"); std::fs::create_dir_all(&nested).unwrap(); - let _ = handle_serve_command(&nested, 8089, "127.0.0.1", false, false, 100, None, false); + assert_served(serve_then_stop( + &nested, + 8089, + "127.0.0.1", + false, + false, + 100, + None, + false, + )); } } From 809da1fbc1d4b5df0964b9f6a7288a65eb0921c7 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 22:32:54 +0200 Subject: [PATCH 019/106] G2BS7: FieldAccess nodes carry their source span, so (obj.f)(3) is a call - create_field_access spans receiver.start..field-name end (was 0..0), incl. tuple index t.0 - try_handle_postfix_call's same-line check now sees the real end; (h.f)(3) no longer splits into h.f; 3 - tests: tests/g2b_s3_misc.rs test_parencall_1_* (committed with G2BS8); hand-mutation span->0..0 fails 5/6 Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 76 ++++++++++++++++---------------- src/frontend/parser/functions.rs | 36 +++++++-------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index dccfef4c9..377ddddeb 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:39.200021013Z", + "created_at": "2026-09-24T00:02:40.736543111Z", "git_context": null, "files": { "./build.rs": { @@ -13055,48 +13055,48 @@ }, "./src/bin/handlers/serve_handler.rs": { "content_hash": [ - 245, - 221, - 133, - 188, - 19, - 172, - 180, - 148, - 96, - 249, - 169, + 205, + 39, 84, - 165, - 95, - 125, - 160, - 211, - 149, - 59, - 238, + 244, 204, - 79, + 62, + 6, + 48, + 248, + 64, + 137, + 101, + 80, + 70, + 133, + 59, + 89, + 76, + 17, + 143, + 188, + 197, + 195, 69, - 157, - 114, - 144, - 74, - 106, - 38, + 112, + 197, 173, - 115, - 88 + 18, + 65, + 124, + 49, + 197 ], "score": { - "structural_complexity": 19.6, + "structural_complexity": 19.3, "semantic_complexity": 17.5, - "duplication_ratio": 16.122448, + "duplication_ratio": 15.62982, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.22244, + "total": 92.42982, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -13108,23 +13108,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 73 duplicate code patterns" + "issue": "Found 89 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.877551, + "amount": 4.37018, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 19.4%" + "issue": "Code duplication: 21.9%" }, { "source_metric": "StructuralComplexity", - "amount": 5.4, + "amount": 5.7000003, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 33" + "issue": "High cognitive complexity: 34" }, { "source_metric": "SemanticComplexity", @@ -44864,7 +44864,7 @@ }, "summary": { "total_files": 528, - "avg_score": 90.33052, + "avg_score": 90.32902, "grade_distribution": { "A+": 195, "A": 176, diff --git a/src/frontend/parser/functions.rs b/src/frontend/parser/functions.rs index ff76506e5..572980a3c 100644 --- a/src/frontend/parser/functions.rs +++ b/src/frontend/parser/functions.rs @@ -443,38 +443,33 @@ pub fn parse_method_call(state: &mut ParserState, receiver: Expr) -> Result { let method = name.clone(); state.tokens.advance(); - parse_method_or_field_access(state, receiver, method) + parse_method_or_field_access(state, receiver, method, name_end) } Some((Token::Send, _)) => { // Handle 'send' as a method name (for actors) state.tokens.advance(); - parse_method_or_field_access(state, receiver, "send".to_string()) + parse_method_or_field_access(state, receiver, "send".to_string(), name_end) } Some((Token::Ask, _)) => { // Handle 'ask' as a method name (for actors) state.tokens.advance(); - parse_method_or_field_access(state, receiver, "ask".to_string()) + parse_method_or_field_access(state, receiver, "ask".to_string(), name_end) } Some((Token::Integer(index), _)) => { // Handle tuple access like t.0, t.1, etc. let index = index.clone(); state.tokens.advance(); - Ok(Expr { - kind: ExprKind::FieldAccess { - object: Box::new(receiver), - field: index, - }, - span: Span { start: 0, end: 0 }, - attributes: Vec::new(), - leading_comments: Vec::new(), - trailing_comment: None, - contracts: Vec::new(), - }) + Ok(create_field_access(receiver, index, name_end)) } _ => { bail!("Expected method name, tuple index, or 'await' after '.'"); @@ -526,6 +521,7 @@ fn parse_method_or_field_access( state: &mut ParserState, receiver: Expr, method: String, + name_end: usize, ) -> Result { // PARSER-069 FIX: Check for turbofish generics (::) before checking for method call // Example: "42".parse::() has :: after parse, not ( @@ -551,7 +547,7 @@ fn parse_method_or_field_access( parse_method_call_access(state, receiver, method_name) } else { // Field access - Ok(create_field_access(receiver, method_name)) + Ok(create_field_access(receiver, method_name, name_end)) } } /// Parse method call with arguments (complexity: 6) @@ -757,14 +753,18 @@ fn create_method_call(receiver: Expr, method: String, args: Vec) -> Expr { contracts: Vec::new(), } } -/// Create a field access expression (complexity: 1) -fn create_field_access(receiver: Expr, field: String) -> Expr { +/// Create a field access expression spanning `receiver.field` (complexity: 1) +/// +/// The span must end at the field name: a postfix call `(obj.f)(x)` is only +/// taken when the `(` is on the same line as the end of its callee (PARENCALL-1). +fn create_field_access(receiver: Expr, field: String, field_end: usize) -> Expr { + let span = Span::new(receiver.span.start.min(field_end), field_end); Expr { kind: ExprKind::FieldAccess { object: Box::new(receiver), field, }, - span: Span { start: 0, end: 0 }, + span, attributes: Vec::new(), leading_comments: Vec::new(), trailing_comment: None, From 7207693c50e6b864968763c080e13fe40dbba500 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 22:33:27 +0200 Subject: [PATCH 020/106] G2BS8: nested pub mod and pub items inside a module parse and transpile - module body parses a pub item via parse_pub_token (pub struct/enum/mod/use/fun, pub(crate)) - transpile_module_declaration emits nested modules as items: pub mod b { .. }, not mod b { { .. } } - tests/g2b_s3_misc.rs: PARENCALL-1 + NESTEDMOD-1 differential (interpreter vs rustc); 10/10 - hand-mutation dropping pub on nested modules fails nestedmod 01/04 (E0603) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 76 ++++---- src/backend/transpiler/program_transpiler.rs | 36 ++-- .../parser/expressions_helpers/modules.rs | 46 ++--- tests/g2b_s3_misc.rs | 181 ++++++++++++++++++ 4 files changed, 257 insertions(+), 82 deletions(-) create mode 100644 tests/g2b_s3_misc.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 377ddddeb..9c00323b6 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:40.736543111Z", + "created_at": "2026-09-24T00:02:42.195626747Z", "git_context": null, "files": { "./build.rs": { @@ -17760,48 +17760,48 @@ }, "./src/frontend/parser/functions.rs": { "content_hash": [ - 154, - 113, - 12, - 169, - 17, - 136, - 7, - 230, - 153, - 174, - 6, - 106, - 189, - 76, - 123, - 146, - 35, - 21, - 64, - 30, - 167, - 224, + 95, + 144, + 172, 186, - 75, - 247, - 173, - 208, - 6, - 102, - 154, - 123, - 17 + 131, + 244, + 161, + 180, + 212, + 137, + 122, + 1, + 128, + 217, + 4, + 253, + 10, + 231, + 137, + 124, + 246, + 254, + 145, + 94, + 168, + 3, + 51, + 57, + 26, + 31, + 186, + 250 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.78125, + "duplication_ratio": 15.897436, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.78125, + "total": 75.89744, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -17813,15 +17813,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 105 duplicate code patterns" + "issue": "Found 104 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.21875, + "amount": 4.1025643, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 21.1%" + "issue": "Code duplication: 20.5%" }, { "source_metric": "StructuralComplexity", @@ -44864,7 +44864,7 @@ }, "summary": { "total_files": 528, - "avg_score": 90.32902, + "avg_score": 90.32924, "grade_distribution": { "A+": 195, "A": 176, diff --git a/src/backend/transpiler/program_transpiler.rs b/src/backend/transpiler/program_transpiler.rs index 3e5cc3391..fc1716623 100644 --- a/src/backend/transpiler/program_transpiler.rs +++ b/src/backend/transpiler/program_transpiler.rs @@ -241,17 +241,10 @@ impl Transpiler { contract_pre_configuration!(name); let module_name = format_ident!("{}", name); let body_tokens = if let ExprKind::Block(exprs) = &body.kind { - let mut module_items = Vec::new(); - for expr in exprs { - match &expr.kind { - ExprKind::Function { .. } => { - module_items.push(self.transpile_function_expr(expr)?); - } - _ => { - module_items.push(self.transpile_expr(expr)?); - } - } - } + let module_items = exprs + .iter() + .map(|expr| self.transpile_module_item(expr)) + .collect::>>()?; quote! { #(#module_items)* } } else { self.transpile_expr(body)? @@ -264,6 +257,27 @@ impl Transpiler { }) } + /// Transpile one item of a module body as a Rust item. + /// + /// A nested module is emitted as an item (`pub mod b { .. }` when marked + /// `pub`), not as an expression whose block body is not an item (NESTEDMOD-1). + /// Complexity: 3 (within Toyota Way limits) + fn transpile_module_item(&self, expr: &Expr) -> Result { + match &expr.kind { + ExprKind::Function { .. } => self.transpile_function_expr(expr), + ExprKind::Module { name, body } => { + let module = self.transpile_module_declaration(name, body)?; + let is_pub = expr.attributes.iter().any(|a| a.name == "pub"); + Ok(if is_pub { + quote! { pub #module } + } else { + module + }) + } + _ => self.transpile_expr(expr), + } + } + /// Transpile statement-only block /// Complexity: 6 (within Toyota Way limits) fn transpile_statement_only_block( diff --git a/src/frontend/parser/expressions_helpers/modules.rs b/src/frontend/parser/expressions_helpers/modules.rs index 181c9928c..96658aa37 100644 --- a/src/frontend/parser/expressions_helpers/modules.rs +++ b/src/frontend/parser/expressions_helpers/modules.rs @@ -29,7 +29,7 @@ //! //! Extracted from expressions.rs to improve maintainability (TDG Structural improvement). -use crate::frontend::ast::{Attribute, Expr, ExprKind, Span}; +use crate::frontend::ast::{Expr, ExprKind, Span}; use crate::frontend::lexer::Token; use crate::frontend::parser::{bail, ParserState, Result}; @@ -88,8 +88,7 @@ fn parse_module_body(state: &mut ParserState) -> Result { let mut exprs = Vec::new(); while !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) { - let is_pub = parse_visibility_modifier(state); - exprs.push(parse_module_item(state, is_pub)?); + exprs.push(parse_module_item(state)?); skip_optional_semicolon(state); } @@ -97,39 +96,20 @@ fn parse_module_body(state: &mut ParserState) -> Result { Ok(Expr::new(ExprKind::Block(exprs), start_span)) } -/// Parse visibility modifier (pub) -fn parse_visibility_modifier(state: &mut ParserState) -> bool { - if matches!(state.tokens.peek(), Some((Token::Pub, _))) { - state.tokens.advance(); - true - } else { - false - } -} - -/// Parse module item (function, use statement, nested module, or expression) -fn parse_module_item(state: &mut ParserState, is_pub: bool) -> Result { +/// Parse module item: a `pub` item or a private item/expression. +/// +/// A `pub` item inside a module is parsed exactly like one at the top level +/// (`pub fun`, `pub struct`, `pub enum`, `pub mod`, `pub use`, `pub(crate) ..`), +/// so nested modules and public types are accepted (NESTEDMOD-1). +fn parse_module_item(state: &mut ParserState) -> Result { match state.tokens.peek() { + Some((Token::Pub, span)) => { + let span = *span; + super::visibility_modifiers::parse_pub_token(state, span) + } // DEFECT-PARSER-015 FIX: Accept both 'fun' and 'fn' for functions Some((Token::Fun | Token::Fn, _)) => { - crate::frontend::parser::functions::parse_function_with_visibility(state, is_pub) - } - Some((Token::Use, _)) if is_pub => { - state.tokens.advance(); - crate::frontend::parser::parse_use_statement_with_visibility(state, true) - } - // DEFECT-PARSER-015 FIX: Allow pub mod - Some((Token::Mod | Token::Module, _)) if is_pub => { - let mut expr = parse_module_declaration(state)?; - expr.attributes.push(Attribute { - name: "pub".to_string(), - args: vec![], - span: expr.span, - }); - Ok(expr) - } - _ if is_pub => { - bail!("'pub' can only be used with function declarations, use statements, or module declarations") + crate::frontend::parser::functions::parse_function_with_visibility(state, false) } _ => crate::frontend::parser::parse_expr_recursive(state), } diff --git a/tests/g2b_s3_misc.rs b/tests/g2b_s3_misc.rs new file mode 100644 index 000000000..c7c7d9be9 --- /dev/null +++ b/tests/g2b_s3_misc.rs @@ -0,0 +1,181 @@ +#![allow(missing_docs)] +//! RHLGA-1 (G2b S3): parser/transpiler defects checked end to end. +//! +//! Each program is run by the interpreter (`ruchy run`) and transpiled, +//! compiled with rustc and executed; both must print the expected output. +//! +//! - PARENCALL-1: `(obj.f)(3)` calls a closure stored in a field. The parser +//! gave FieldAccess nodes a 0..0 span, so the same-line check for a postfix +//! call failed and `(h.f)(3)` was split into `h.f; 3`. +//! - NESTEDMOD-1: `pub mod b { .. }` inside `mod a { .. }` and `pub struct` +//! inside a module. + +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] + +use ruchy::frontend::ast::{Expr, ExprKind}; +use ruchy::{Parser, Transpiler}; +use std::process::Command; + +fn scratch_dir(name: &str) -> tempfile::TempDir { + tempfile::Builder::new() + .prefix(&format!("g2b_s3_{name}_")) + .tempdir() + .expect("scratch dir") +} + +fn compile_and_run(rust: &str, name: &str) -> String { + let dir = scratch_dir(name); + let src = dir.path().join("main.rs"); + let bin = dir.path().join("main"); + std::fs::write(&src, rust).expect("write rust"); + let out = Command::new("rustc") + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&src) + .output() + .expect("rustc runs"); + assert!( + out.status.success(), + "rustc rejected the transpiled program:\n{}\n--- rust ---\n{rust}", + String::from_utf8_lossy(&out.stderr) + ); + let run = Command::new(&bin).output().expect("binary runs"); + assert!(run.status.success(), "binary failed: {run:?}"); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +fn ruchy_run(code: &str, name: &str) -> String { + let dir = scratch_dir(name); + let file = dir.path().join("prog.ruchy"); + std::fs::write(&file, code).expect("write ruchy"); + let out = Command::new(env!("CARGO_BIN_EXE_ruchy")) + .arg("run") + .arg(&file) + .output() + .expect("ruchy runs"); + assert!(out.status.success(), "ruchy run failed: {out:?}"); + String::from_utf8_lossy(&out.stdout).into_owned() +} + +fn parse(code: &str) -> Expr { + Parser::new(code).parse().expect("parses") +} + +/// Interpreter and transpiled binary both print `expected`. +fn differential(code: &str, name: &str, expected: &str) -> String { + let rust = Transpiler::new() + .transpile_to_program(&parse(code)) + .expect("transpiles") + .to_string(); + assert_eq!(ruchy_run(code, name).trim(), expected, "interpreter"); + assert_eq!(compile_and_run(&rust, name).trim(), expected, "{rust}"); + rust +} + +// ===== PARENCALL-1 ===== + +const PARENCALL_IN_MAIN: &str = "struct H { f: fn(i32) -> i32 }\n\ +fun main() {\n let h = H { f: |x| x * 2 }\n let r = (h.f)(3)\n println(r)\n}\n"; + +#[test] +fn test_parencall_1_01_field_closure_call_in_block() { + differential(PARENCALL_IN_MAIN, "parencall_block", "6"); +} + +#[test] +fn test_parencall_1_02_field_closure_call_as_argument() { + let code = "struct H { f: fn(i32) -> i32 }\n\ +fun main() {\n let h = H { f: |x| x * 2 }\n println((h.f)(3))\n}\n"; + differential(code, "parencall_arg", "6"); +} + +#[test] +fn test_parencall_1_03_top_level_after_let() { + let code = "let h = { f: |x| x * 2 }\nprintln((h.f)(3))\n"; + assert_eq!(ruchy_run(code, "parencall_top").trim(), "6"); +} + +/// The parsed `let r = (h.f)(3)` binds a Call whose callee is the field. +#[test] +fn test_parencall_1_04_ast_is_a_call_on_the_field() { + let ast = parse("let h = 1\nlet r = (h.f)(3)\nr"); + let text = format!("{ast:?}"); + assert!( + text.contains("Call { func: Expr { kind: FieldAccess"), + "not a call on the field: {text}" + ); +} + +/// A field access carries the source span of `receiver.field`. +#[test] +fn test_parencall_1_05_field_access_span_covers_source() { + let src = "obj.field"; + let ast = parse(src); + assert!(matches!(ast.kind, ExprKind::FieldAccess { .. }), "{ast:?}"); + assert_eq!((ast.span.start, ast.span.end), (0, src.len())); + let tuple = parse("t.0"); + assert_eq!((tuple.span.start, tuple.span.end), (0, 3), "{tuple:?}"); +} + +/// A parenthesised field on one line and a parenthesised expression on the +/// next stay two statements: the postfix call still requires the same line. +#[test] +fn test_parencall_1_06_paren_on_next_line_is_not_a_call() { + let ast = parse("let h = 1\n(h.f)\n(3)"); + let text = format!("{ast:?}"); + assert!( + !text.contains("Call {"), + "split lines became a call: {text}" + ); +} + +// ===== NESTEDMOD-1 ===== + +const NESTED_MOD: &str = "mod a {\n pub fun g() -> i32 { 1 }\n pub mod b {\n \ +pub fun f() -> i32 { 41 }\n }\n}\n\ +fun main() {\n println(a::b::f() + a::g())\n}\n"; + +#[test] +fn test_nestedmod_1_01_nested_pub_mod_path_call() { + let rust = differential(NESTED_MOD, "nested_mod", "42"); + assert!(rust.contains("pub mod b"), "{rust}"); +} + +/// `pub struct` inside a module parses (it was refused: "'pub' can only be +/// used with function declarations ..."), and the transpiled module keeps the +/// struct and its `pub` field public. The interpreter run avoids constructing +/// the struct inside the module (see the receipt: runtime struct lookup). +#[test] +fn test_nestedmod_1_02_pub_struct_inside_mod() { + let code = "mod shapes {\n pub struct P { pub x: i32 }\n \ +pub fun make() -> P { P { x: 5 } }\n}\n\ +fun main() {\n let p = shapes::make()\n println(p.x)\n}\n"; + let rust = Transpiler::new() + .transpile_to_program(&parse(code)) + .expect("transpiles") + .to_string(); + assert!(rust.contains("pub struct P"), "{rust}"); + assert_eq!(compile_and_run(&rust, "pub_struct").trim(), "5", "{rust}"); + let interp = "mod shapes {\n pub struct P { pub x: i32 }\n \ +pub fun five() -> i32 { 5 }\n}\n\ +fun main() {\n println(shapes::five())\n}\n"; + assert_eq!(ruchy_run(interp, "pub_struct_run").trim(), "5"); +} + +#[test] +fn test_nestedmod_1_03_private_nested_mod_used_inside_parent() { + let code = "mod a {\n mod inner {\n pub fun v() -> i32 { 7 }\n }\n \ +pub fun g() -> i32 { inner::v() * 2 }\n}\n\ +fun main() {\n println(a::g())\n}\n"; + let rust = differential(code, "private_nested", "14"); + assert!(!rust.contains("pub mod inner"), "{rust}"); +} + +#[test] +fn test_nestedmod_1_04_three_levels() { + let code = "mod a {\n pub mod b {\n pub mod c {\n \ +pub fun f() -> i32 { 3 }\n }\n }\n}\n\ +fun main() {\n println(a::b::c::f())\n}\n"; + differential(code, "three_levels", "3"); +} From 07ba78cda6fe6a7fcc826f8b77cef75653b2406d Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 22:38:18 +0200 Subject: [PATCH 021/106] G2BT10: count/sum/min/max run on a range itself; arrays keep .iter() Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 154 ++++++++--------- src/backend/transpiler/call_transpilation.rs | 75 +++++--- .../bug_032_range_function_not_transpiled.rs | 6 +- tests/g2b_t3_misc.rs | 160 ++++++++++++++++++ 4 files changed, 297 insertions(+), 98 deletions(-) create mode 100644 tests/g2b_t3_misc.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 9c00323b6..bc3e18a16 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:42.195626747Z", + "created_at": "2026-09-24T00:02:44.094721892Z", "git_context": null, "files": { "./build.rs": { @@ -7750,48 +7750,48 @@ }, "./src/backend/transpiler/program_transpiler.rs": { "content_hash": [ - 160, - 209, - 255, - 12, - 153, - 198, - 132, - 226, - 142, - 128, - 150, - 178, - 67, - 95, - 86, - 186, - 103, - 154, - 1, - 52, - 145, - 103, - 180, - 212, - 151, - 26, - 192, - 68, - 223, - 80, - 38, - 223 + 227, + 183, + 19, + 21, + 187, + 175, + 246, + 135, + 174, + 123, + 58, + 123, + 51, + 231, + 139, + 54, + 27, + 93, + 143, + 78, + 228, + 251, + 134, + 33, + 187, + 17, + 18, + 55, + 20, + 46, + 66, + 33 ], "score": { - "structural_complexity": 9.0, + "structural_complexity": 9.1, "semantic_complexity": 20.0, - "duplication_ratio": 12.835052, + "duplication_ratio": 12.883959, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 81.83505, + "total": 81.98396, "grade": "B+", "confidence": 1.0, "language": "Rust", @@ -7803,31 +7803,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 122 duplicate code patterns" + "issue": "Found 124 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 7.1649485, + "amount": 7.116041, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 35.8%" + "issue": "Code duplication: 35.6%" }, { "source_metric": "StructuralComplexity", - "amount": 7.5000005, + "amount": 6.9, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 40" + "issue": "High cognitive complexity: 38" }, { "source_metric": "StructuralComplexity", - "amount": 8.5, + "amount": 9.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 47" + "issue": "High cyclomatic complexity: 48" } ], "critical_defects_count": 0, @@ -16779,38 +16779,38 @@ }, "./src/frontend/parser/expressions_helpers/modules.rs": { "content_hash": [ - 214, - 90, - 108, - 125, - 169, - 95, - 18, - 74, - 31, - 98, - 251, - 59, - 230, - 67, - 131, - 52, - 130, - 202, - 29, - 209, - 68, - 64, - 42, - 92, - 119, - 130, - 122, - 28, - 16, - 131, - 61, - 117 + 101, + 155, + 19, + 223, + 83, + 45, + 196, + 242, + 218, + 62, + 104, + 153, + 221, + 227, + 40, + 73, + 192, + 96, + 91, + 49, + 41, + 57, + 135, + 78, + 254, + 44, + 134, + 140, + 106, + 229, + 161, + 253 ], "score": { "structural_complexity": 25.0, @@ -16832,7 +16832,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 13 duplicate code patterns" + "issue": "Found 12 duplicate code patterns" } ], "critical_defects_count": 0, @@ -44864,7 +44864,7 @@ }, "summary": { "total_files": 528, - "avg_score": 90.32924, + "avg_score": 90.32952, "grade_distribution": { "A+": 195, "A": 176, diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index 242cf6db3..bed871fef 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -356,6 +356,54 @@ impl Transpiler { ) } + /// RHLGA-1: a range receiver (`a..b`, `range(a, b)`) is already an iterator. + fn is_range_receiver(object: &Expr) -> bool { + match &object.kind { + ExprKind::Range { .. } => true, + ExprKind::Call { func, args } => { + matches!(&func.kind, ExprKind::Identifier(name) if name == "range") + && matches!(args.len(), 1 | 2) + } + _ => false, + } + } + + /// `a..b` needs parentheses to take a method; `range(a, b)` emits them itself. + fn parenthesize_range_literal(obj_tokens: &TokenStream, object: &Expr) -> TokenStream { + if matches!(object.kind, ExprKind::Range { .. }) { + quote! { (#obj_tokens) } + } else { + obj_tokens.clone() + } + } + + /// `count`/`sum`/`min`/`max` on a non-DataFrame receiver. + /// A collection is iterated by reference (`.iter()`, `.copied()` for + /// min/max); a range is called directly, parenthesised when it is a + /// range literal so the method binds to the whole range. + fn transpile_collection_aggregate( + obj_tokens: &TokenStream, + method: &str, + method_ident: &proc_macro2::Ident, + object: &Expr, + ) -> TokenStream { + let (iter, copied) = if Self::is_range_receiver(object) { + let range = Self::parenthesize_range_literal(obj_tokens, object); + (range.clone(), range) + } else { + ( + quote! { #obj_tokens.iter() }, + quote! { #obj_tokens.iter().copied() }, + ) + }; + match method { + // TRANSPILER-ITERATOR-001: sum defaults to i32 (collect loses the type) + "sum" => quote! { #iter.sum::() }, + "count" => quote! { #iter.count() }, + _ => quote! { #copied.#method_ident() }, + } + } + /// Dispatch method call by category pub(super) fn dispatch_method_by_category( &self, @@ -387,29 +435,16 @@ impl Transpiler { Ok(quote! { #obj_tokens.#method_ident(#(#arg_tokens),*) }) } // Aggregation methods that work on both DataFrame and collections - "sum" => { - if Transpiler::is_dataframe_expr(object) { - self.transpile_dataframe_method(object, method, &[]) - } else { - // TRANSPILER-ITERATOR-001 FIX: For collections, use .iter().sum::() - // Default to i32 for sum since collect::>() loses type info - Ok(quote! { #obj_tokens.iter().sum::() }) - } - } - "min" | "max" => { - if Transpiler::is_dataframe_expr(object) { - self.transpile_dataframe_method(object, method, &[]) - } else { - // min/max return Option, use .copied() for ownership - Ok(quote! { #obj_tokens.iter().copied().#method_ident() }) - } - } - "count" => { + "sum" | "min" | "max" | "count" => { if Transpiler::is_dataframe_expr(object) { self.transpile_dataframe_method(object, method, &[]) } else { - // count returns usize - Ok(quote! { #obj_tokens.iter().count() }) + Ok(Self::transpile_collection_aggregate( + obj_tokens, + method, + method_ident, + object, + )) } } // DataFrame-only operations diff --git a/tests/bug_032_range_function_not_transpiled.rs b/tests/bug_032_range_function_not_transpiled.rs index 5ef3d2b94..555a75cfc 100644 --- a/tests/bug_032_range_function_not_transpiled.rs +++ b/tests/bug_032_range_function_not_transpiled.rs @@ -126,13 +126,17 @@ fun main() { fs::write(&source, code).expect("Failed to write test file"); + let binary = temp.path().join("test_binary"); ruchy_cmd() .arg("compile") .arg(&source) .arg("-o") - .arg(temp.path().join("test_binary")) + .arg(&binary) .assert() .success(); + + // RHLGA-1: a range is already an iterator; the binary counts it. + Command::new(&binary).assert().success().stdout("10\n"); } /// Test 5: Multiple `range()` calls diff --git a/tests/g2b_t3_misc.rs b/tests/g2b_t3_misc.rs new file mode 100644 index 000000000..ce441b8d9 --- /dev/null +++ b/tests/g2b_t3_misc.rs @@ -0,0 +1,160 @@ +//! G2B-T10/T12 (RHLGA-1): range aggregations and Rust-compatible `{:?}`. +//! +//! T10: `count`/`sum`/`min`/`max` on a range receiver (`range(a, b)` or +//! `a..b`) are called on the range itself; a range has no `.iter()`. +//! T12: the interpreter renders `{:?}` exactly as rustc renders the +//! transpiled value, in `println!`, `format!` and the `println(..)` function. +//! Each program is transpiled, compiled with rustc and run; its output is +//! compared with `ruchy run` where the interpreter accepts the program. + +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] + +use ruchy::{Parser, Transpiler}; +use std::process::Command; + +fn scratch_dir(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("g2b_t3_{}_{name}", std::process::id())); + std::fs::create_dir_all(&dir).expect("scratch dir"); + dir +} + +fn transpile(code: &str) -> String { + let ast = Parser::new(code).parse().expect("parses"); + Transpiler::new() + .transpile_to_program(&ast) + .expect("transpiles") + .to_string() +} + +fn compiled_output(code: &str, name: &str) -> String { + let rust = transpile(code); + let dir = scratch_dir(name); + let src = dir.join("main.rs"); + let bin = dir.join("main"); + std::fs::write(&src, &rust).expect("write rust"); + let out = Command::new("rustc") + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&src) + .output() + .expect("rustc runs"); + assert!( + out.status.success(), + "rustc rejected the transpiled program:\n{}\n--- rust ---\n{rust}", + String::from_utf8_lossy(&out.stderr) + ); + let run = Command::new(&bin).output().expect("binary runs"); + assert!(run.status.success(), "binary failed: {run:?}"); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +fn interpreted_output(code: &str, name: &str) -> String { + let file = scratch_dir(name).join("prog.ruchy"); + std::fs::write(&file, code).expect("write ruchy"); + let out = Command::new(env!("CARGO_BIN_EXE_ruchy")) + .arg("run") + .arg(&file) + .output() + .expect("ruchy runs"); + assert!(out.status.success(), "ruchy run failed: {out:?}"); + String::from_utf8_lossy(&out.stdout).into_owned() +} + +fn assert_same_output(code: &str, name: &str, expected: &str) { + let compiled = compiled_output(code, name); + let interpreted = interpreted_output(code, name); + assert_eq!(compiled, expected, "rustc output for:\n{code}"); + assert_eq!(interpreted, compiled, "interpreter vs rustc for:\n{code}"); +} + +// ---------------------------------------------------------------- T10 ranges + +#[test] +fn test_g2bt10_range_call_count_compiles_and_counts() { + let code = "fun main() {\n let count = range(0, 10).count()\n println(count)\n}\n"; + assert_eq!(compiled_output(code, "t10_count"), "10\n"); +} + +#[test] +fn test_g2bt10_range_call_sum_matches_interpreter() { + let code = "fun main() {\n let total = range(0, 10).sum()\n println(total)\n}\n"; + assert_same_output(code, "t10_sum", "45\n"); +} + +#[test] +fn test_g2bt10_range_literal_count_and_sum_compile() { + let code = "fun main() {\n let c = (0..10).count()\n let s = (1..=4).sum()\n println(c)\n println(s)\n}\n"; + assert_eq!(compiled_output(code, "t10_literal"), "10\n10\n"); +} + +#[test] +fn test_g2bt10_range_min_max_compile() { + let code = "fun main() {\n let lo = range(3, 9).min()\n let hi = range(3, 9).max()\n println(lo)\n println(hi)\n}\n"; + assert_eq!(compiled_output(code, "t10_minmax"), "Some(3)\nSome(8)\n"); +} + +#[test] +fn test_g2bt10_array_receiver_keeps_iter() { + let code = "fun main() {\n let total = [1, 2, 3].sum()\n println(total)\n}\n"; + assert!(transpile(code).contains(". iter () . sum")); + assert_same_output(code, "t10_array", "6\n"); +} + +// ------------------------------------------------------------- T12 {:?} Debug + +const DEBUG_TABLE: &str = r#"fun main() { + println!("{:?}", [1, 2]) + println!("{:?}", "hi") + println!("{:?}", "a\"b\n") + println!("{:?}", (1, true)) + println!("{:?}", 2.0) + println!("{:?}", 1.5) + println!("{:?}", -3) + println!("{:?}", [[1, 0], [2, 3]]) + println!("{:?}", Some(3)) + println!("{:?}", true) + println!("{:?}", [(1, "x")]) + println!("n={:?} s={:?} d={}", 7, "q", "r") + let v: Option = None + println!("{:?}", v) + let r: Result = Ok(1) + println!("{:?}", r) + println!("{:?}", 0..3) +} +"#; + +const DEBUG_EXPECTED: &str = r#"[1, 2] +"hi" +"a\"b\n" +(1, true) +2.0 +1.5 +-3 +[[1, 0], [2, 3]] +Some(3) +true +[(1, "x")] +n=7 s="q" d=r +None +Ok(1) +0..3 +"#; + +#[test] +fn test_g2bt12_println_macro_debug_matches_rustc() { + assert_same_output(DEBUG_TABLE, "t12_table", DEBUG_EXPECTED); +} + +#[test] +fn test_g2bt12_format_macro_matches_rustc() { + let code = "fun main() {\n let s = format!(\"{:?}|{}-{}\", [\"a\", \"b\"], \"x\", 1)\n println!(\"{}\", s)\n}\n"; + assert_same_output(code, "t12_format", "[\"a\", \"b\"]|x-1\n"); +} + +#[test] +fn test_g2bt12_println_function_substitutes_like_macro() { + let code = "fun main() {\n println(\"{:?}\", [1, 2])\n println(\"{} {:?} {}\", \"a\", \"b\", 3.5)\n println(\"{}+{}\", 1, 2)\n}\n"; + let expected = "[1, 2]\na \"b\" 3.5\n1+2\n"; + assert_eq!(interpreted_output(code, "t12_fn"), expected); +} From 7da33b7b1c53c8e3eba226061726713775c8fcd2 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 23 Sep 2026 22:38:22 +0200 Subject: [PATCH 022/106] G2BT12: {:?} in the interpreter prints what rustc prints; println() and format! substitute like the macro Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 78 ++++++------ src/runtime/eval_builtin.rs | 14 +-- src/runtime/interpreter_misc_eval.rs | 68 +---------- src/runtime/value_format.rs | 175 ++++++++++++++++++++++++--- 4 files changed, 204 insertions(+), 131 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index bc3e18a16..685746ff7 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:44.094721892Z", + "created_at": "2026-09-24T00:02:46.386806605Z", "git_context": null, "files": { "./build.rs": { @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 204, - 33, - 62, - 45, - 32, - 234, - 7, - 201, - 223, - 162, - 166, - 30, - 242, - 128, - 178, - 2, + 230, + 38, + 235, + 39, + 143, + 35, 197, - 18, - 187, - 80, - 5, - 99, - 202, - 33, - 82, - 48, - 236, - 15, - 135, - 37, - 207, - 210 + 59, + 43, + 241, + 228, + 103, + 210, + 206, + 64, + 195, + 34, + 197, + 242, + 161, + 139, + 205, + 240, + 40, + 26, + 7, + 54, + 121, + 74, + 43, + 132, + 151 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.753769, + "duplication_ratio": 15.878788, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.75377, + "total": 75.878784, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3639,15 +3639,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 65 duplicate code patterns" + "issue": "Found 68 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.246231, + "amount": 4.121212, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 21.2%" + "issue": "Code duplication: 20.6%" }, { "source_metric": "StructuralComplexity", @@ -3663,7 +3663,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 64" + "issue": "High cyclomatic complexity: 68" } ], "critical_defects_count": 0, @@ -44864,7 +44864,7 @@ }, "summary": { "total_files": 528, - "avg_score": 90.32952, + "avg_score": 90.32976, "grade_distribution": { "A+": 195, "A": 176, diff --git a/src/runtime/eval_builtin.rs b/src/runtime/eval_builtin.rs index a7453ea3e..80af7331c 100644 --- a/src/runtime/eval_builtin.rs +++ b/src/runtime/eval_builtin.rs @@ -297,16 +297,9 @@ fn format_value_for_println(value: &Value) -> String { } } -/// Format string with interpolation -/// Complexity: 2 (within Toyota Way limits) +/// Format string with interpolation: `{}` raw, `{:?}` Rust Debug fn format_with_interpolation(fmt_str: &str, args: &[Value]) -> String { - let mut result = fmt_str.to_string(); - for arg in args { - if let Some(pos) = result.find("{}") { - result.replace_range(pos..pos + 2, &format_value_for_println(arg)); - } - } - result + crate::runtime::value_format::format_string_with_values(fmt_str, args) } /// Join values with spaces @@ -324,7 +317,8 @@ fn format_println_output(args: &[Value]) -> String { if args.is_empty() { "\n".to_string() } else if let Value::String(fmt_str) = &args[0] { - if fmt_str.contains("{}") { + // RHLGA-1: the function form substitutes {} and {:?} like println!() + if fmt_str.contains("{}") || fmt_str.contains("{:?}") { format!("{}\n", format_with_interpolation(fmt_str, &args[1..])) } else { format!("{}\n", join_values(args)) diff --git a/src/runtime/interpreter_misc_eval.rs b/src/runtime/interpreter_misc_eval.rs index 6a41fee4d..b1e460d25 100644 --- a/src/runtime/interpreter_misc_eval.rs +++ b/src/runtime/interpreter_misc_eval.rs @@ -280,9 +280,9 @@ fn eval_format_macro(interp: &mut Interpreter, args: &[Expr]) -> Result Result>, - result: &mut String, - values: &[Value], - value_index: &mut usize, -) -> bool { - if chars.peek() != Some(&':') { - return false; - } - chars.next(); - if chars.peek() != Some(&'?') { - result.push_str("{:"); - return true; - } - chars.next(); - if chars.peek() != Some(&'}') { - result.push_str("{:?"); - return true; - } - chars.next(); - if *value_index < values.len() { - result.push_str(&format!("{:?}", values[*value_index])); - *value_index += 1; - } else { - result.push_str("{:?}"); - } - true -} - -fn format_with_placeholders(format_str: &str, values: &[Value]) -> String { - let mut result = String::new(); - let mut chars = format_str.chars().peekable(); - let mut value_index = 0; - - while let Some(ch) = chars.next() { - if ch != '{' { - result.push(ch); - continue; - } - if try_consume_debug_placeholder(&mut chars, &mut result, values, &mut value_index) { - continue; - } - if chars.peek() == Some(&'}') { - chars.next(); - if value_index < values.len() { - result.push_str(&values[value_index].to_string()); - value_index += 1; - } else { - result.push_str("{}"); - } - } else { - result.push(ch); - } - } - - result -} - // ============================================================================ // Try operator (?) // ============================================================================ diff --git a/src/runtime/value_format.rs b/src/runtime/value_format.rs index 86930dc31..90715b071 100644 --- a/src/runtime/value_format.rs +++ b/src/runtime/value_format.rs @@ -20,7 +20,7 @@ pub fn format_string_with_values(format_str: &str, values: &[Value]) -> String { if chars.peek() == Some(&'}') { chars.next(); if value_index < values.len() { - result.push_str(&format!("{:?}", values[value_index])); + result.push_str(&rust_debug(&values[value_index])); value_index += 1; } else { result.push_str("{:?}"); @@ -74,9 +74,111 @@ pub fn format_value_with_spec(value: &Value, spec: &str) -> String { value.to_string() } -/// Format value for debug output +/// Format value for debug output (`{:?}`), as rustc prints the transpiled value. pub fn format_value_debug(value: &Value) -> String { - format!("{value:?}") + rust_debug(value) +} + +/// RHLGA-1: render a value the way Rust's `{:?}` renders the transpiled value +/// (`[1, 2]`, `"hi"`, `Some(3)`, `(1, true)`, `2.0`), not the internal enum. +/// Struct/object field order is not recorded in a `Value`; keys are sorted. +pub fn rust_debug(value: &Value) -> String { + debug_scalar(value).unwrap_or_else(|| debug_compound(value)) +} + +fn debug_scalar(value: &Value) -> Option { + match value { + Value::Integer(i) => Some(i.to_string()), + Value::Float(f) => Some(format!("{f:?}")), + Value::Bool(b) => Some(b.to_string()), + Value::Byte(b) => Some(b.to_string()), + Value::Nil => Some("()".to_string()), + Value::String(s) => Some(format!("{:?}", s.as_ref())), + _ => None, + } +} + +fn debug_compound(value: &Value) -> String { + match value { + Value::Array(items) => format!("[{}]", debug_list(items)), + Value::Tuple(items) => debug_tuple(items), + Value::EnumVariant { + variant_name, data, .. + } => debug_variant(variant_name, data.as_deref()), + Value::Struct { name, fields } => debug_named_fields(name, fields), + Value::Object(map) => debug_object_map(map), + Value::ObjectMut(cell) => cell + .lock() + .map_or_else(|_| value.to_string(), |map| debug_object_map(&map)), + Value::Class { + class_name, fields, .. + } => fields.read().map_or_else( + |_| value.to_string(), + |map| debug_named_fields(class_name, &map), + ), + Value::Range { + start, + end, + inclusive, + } => debug_range(start, end, *inclusive), + _ => value.to_string(), + } +} + +fn debug_list(items: &[Value]) -> String { + items.iter().map(rust_debug).collect::>().join(", ") +} + +fn debug_tuple(items: &[Value]) -> String { + if items.len() == 1 { + format!("({},)", rust_debug(&items[0])) + } else { + format!("({})", debug_list(items)) + } +} + +fn debug_variant(name: &str, data: Option<&[Value]>) -> String { + match data { + Some(values) if !values.is_empty() => format!("{name}({})", debug_list(values)), + _ => name.to_string(), + } +} + +fn debug_range(start: &Value, end: &Value, inclusive: bool) -> String { + let op = if inclusive { "..=" } else { ".." }; + format!("{}{op}{}", rust_debug(start), rust_debug(end)) +} + +fn sorted_entries(map: &std::collections::HashMap) -> Vec<(&String, &Value)> { + let mut entries: Vec<_> = map.iter().filter(|(k, _)| !k.starts_with("__")).collect(); + entries.sort_by(|a, b| a.0.cmp(b.0)); + entries +} + +/// `Name { a: 1, b: 2 }` (derived Debug); a field-less struct prints `Name`. +fn debug_named_fields(name: &str, map: &std::collections::HashMap) -> String { + let fields: Vec = sorted_entries(map) + .into_iter() + .map(|(k, v)| format!("{k}: {}", rust_debug(v))) + .collect(); + if fields.is_empty() { + name.to_string() + } else { + format!("{name} {{ {} }}", fields.join(", ")) + } +} + +/// An object literal transpiles to a `BTreeMap`: `{"a": 1}`. An instance +/// carrying `__class` prints as its struct. +fn debug_object_map(map: &std::collections::HashMap) -> String { + if let Some(Value::String(class)) = map.get("__class") { + return debug_named_fields(class, map); + } + let entries: Vec = sorted_entries(map) + .into_iter() + .map(|(k, v)| format!("{k:?}: {}", rust_debug(v))) + .collect(); + format!("{{{}}}", entries.join(", ")) } /// Format value for display output (no extra quotes for strings) @@ -114,7 +216,7 @@ mod tests { #[test] fn test_format_debug_placeholder() { let result = format_string_with_values("{:?}", &[Value::from_string("test".to_string())]); - assert!(result.contains("String")); + assert_eq!(result, "\"test\""); } #[test] @@ -243,13 +345,13 @@ mod tests { #[test] fn test_debug_integer() { let result = format_value_debug(&Value::Integer(42)); - assert!(result.contains("42")); + assert_eq!(result, "42"); } #[test] fn test_debug_string() { - let result = format_value_debug(&Value::from_string("test".to_string())); - assert!(result.contains("String")); + let result = format_value_debug(&Value::from_string("a\"b".to_string())); + assert_eq!(result, "\"a\\\"b\""); } #[test] @@ -258,7 +360,7 @@ mod tests { vec![Value::Integer(1), Value::Integer(2)].into_boxed_slice(), )); let result = format_value_debug(&arr); - assert!(result.contains("Array")); + assert_eq!(result, "[1, 2]"); } // format_value_display tests @@ -340,20 +442,19 @@ mod tests { #[test] fn test_format_debug_nil() { let result = format_value_debug(&Value::Nil); - assert!(result.contains("Nil")); + assert_eq!(result, "()"); } #[test] fn test_format_debug_bool() { let result = format_value_debug(&Value::Bool(false)); - assert!(result.contains("Bool")); - assert!(result.contains("false")); + assert_eq!(result, "false"); } #[test] fn test_format_debug_float() { - let result = format_value_debug(&Value::Float(2.718)); - assert!(result.contains("Float")); + assert_eq!(format_value_debug(&Value::Float(2.718)), "2.718"); + assert_eq!(format_value_debug(&Value::Float(2.0)), "2.0"); } #[test] @@ -366,7 +467,7 @@ mod tests { #[test] fn test_format_consecutive_debug_placeholders() { let result = format_string_with_values("{:?}{:?}", &[Value::Integer(1), Value::Integer(2)]); - assert!(result.contains("Integer")); + assert_eq!(result, "12"); } #[test] @@ -491,7 +592,7 @@ mod tests { fn test_format_debug_tuple() { let tuple = Value::Tuple(Arc::from(vec![Value::Integer(1)])); let result = format_value_debug(&tuple); - assert!(result.contains("Tuple")); + assert_eq!(result, "(1,)"); } #[test] @@ -507,8 +608,46 @@ mod tests { "{:?} {:?} {:?}", &[Value::Integer(1), Value::Float(2.0), Value::Bool(true)], ); - assert!(result.contains("Integer")); - assert!(result.contains("Float")); - assert!(result.contains("Bool")); + assert_eq!(result, "1 2.0 true"); + } + + #[test] + fn test_rust_debug_compound_values() { + let some = Value::EnumVariant { + enum_name: "Option".to_string(), + variant_name: "Some".to_string(), + data: Some(vec![Value::Integer(3)]), + }; + let none = Value::EnumVariant { + enum_name: "Option".to_string(), + variant_name: "None".to_string(), + data: None, + }; + assert_eq!(rust_debug(&some), "Some(3)"); + assert_eq!(rust_debug(&none), "None"); + let pair = Value::Tuple(Arc::from(vec![Value::Integer(1), Value::Bool(true)])); + assert_eq!(rust_debug(&pair), "(1, true)"); + let range = Value::Range { + start: Box::new(Value::Integer(0)), + end: Box::new(Value::Integer(3)), + inclusive: true, + }; + assert_eq!(rust_debug(&range), "0..=3"); + } + + #[test] + fn test_rust_debug_struct_and_object() { + let mut fields = std::collections::HashMap::new(); + fields.insert("y".to_string(), Value::Integer(2)); + fields.insert("x".to_string(), Value::from_string("s".to_string())); + let point = Value::Struct { + name: "P".to_string(), + fields: Arc::new(fields.clone()), + }; + assert_eq!(rust_debug(&point), "P { x: \"s\", y: 2 }"); + assert_eq!( + rust_debug(&Value::Object(Arc::new(fields))), + "{\"x\": \"s\", \"y\": 2}" + ); } } From b4213b4cd54a1e55bb9998e4277f284d935d3e98 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 02:05:59 +0200 Subject: [PATCH 023/106] RHLGA-1: G2 part 2 CHANGELOG; the exhaustive router covers explain (added on main by #243) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 3963 ++++++++++++---------------- CHANGELOG.md | 33 + src/bin/handlers/command_router.rs | 1 + 3 files changed, 1656 insertions(+), 2341 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 685746ff7..61648ed1a 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T00:02:46.386806605Z", + "created_at": "2026-09-23T14:08:18.278349727Z", "git_context": null, "files": { "./build.rs": { @@ -1334,48 +1334,48 @@ }, "./ruchy-embed/src/lib.rs": { "content_hash": [ - 191, - 153, - 206, - 223, - 218, + 182, 172, - 23, - 97, - 25, - 67, - 114, - 162, - 203, - 157, - 5, - 149, - 54, + 9, + 144, + 64, + 168, + 166, + 252, + 142, + 215, + 221, + 36, 161, - 3, - 242, - 62, - 176, + 204, + 97, + 160, + 119, + 118, + 121, + 78, + 169, + 166, 237, - 73, - 151, - 70, - 34, - 93, - 38, - 214, - 74, - 153 + 148, + 212, + 22, + 252, + 240, + 54, + 46, + 174, + 133 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.391304, + "duplication_ratio": 17.336245, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.08301, + "total": 93.03295, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -1391,11 +1391,11 @@ }, { "source_metric": "Duplication", - "amount": 2.6086955, + "amount": 2.6637554, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.0%" + "issue": "Code duplication: 13.3%" } ], "critical_defects_count": 0, @@ -2432,48 +2432,48 @@ }, "./src/backend/compiler.rs": { "content_hash": [ - 83, - 156, - 162, - 40, - 232, + 47, + 42, + 157, + 120, + 86, + 178, + 224, + 81, 231, - 196, - 254, - 16, - 191, - 22, - 26, - 9, - 245, - 237, - 199, - 5, - 161, - 104, - 171, - 117, - 219, - 127, - 167, - 13, - 180, - 90, 68, - 248, - 6, - 141, - 186 + 145, + 202, + 135, + 30, + 169, + 48, + 38, + 112, + 137, + 21, + 147, + 96, + 83, + 203, + 80, + 19, + 208, + 96, + 128, + 119, + 59, + 90 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.601732, + "duplication_ratio": 16.601307, "coupling_score": 14.6, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.20173, + "total": 76.20131, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -2485,11 +2485,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 83 duplicate code patterns" + "issue": "Found 81 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.3982685, + "amount": 3.3986928, "applied_to": [ "Duplication" ], @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 230, - 38, - 235, - 39, - 143, - 35, 197, - 59, - 43, - 241, - 228, - 103, - 210, - 206, - 64, - 195, - 34, - 197, - 242, - 161, - 139, - 205, - 240, - 40, - 26, + 159, + 185, + 63, + 204, + 21, + 95, + 14, + 229, + 178, + 11, + 42, + 232, + 65, + 23, + 130, + 204, + 52, + 101, + 12, + 81, + 204, + 147, + 10, + 22, + 167, + 110, 7, - 54, - 121, - 74, - 43, - 132, - 151 + 7, + 219, + 109, + 146 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.878788, + "duplication_ratio": 15.546772, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.878784, + "total": 75.54677, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3639,15 +3639,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 68 duplicate code patterns" + "issue": "Found 65 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.121212, + "amount": 4.453228, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.6%" + "issue": "Code duplication: 22.3%" }, { "source_metric": "StructuralComplexity", @@ -3655,15 +3655,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 68" + "issue": "High cognitive complexity: 64" }, { "source_metric": "StructuralComplexity", - "amount": 15.0, + "amount": 14.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 68" + "issue": "High cyclomatic complexity: 58" } ], "critical_defects_count": 0, @@ -5317,38 +5317,38 @@ }, "./src/backend/transpiler/expressions.rs": { "content_hash": [ - 90, - 75, - 96, - 171, - 202, - 232, - 119, - 123, - 162, - 59, - 125, - 248, - 37, - 116, - 65, - 225, - 184, - 228, - 221, - 174, - 167, + 88, 174, - 8, - 0, - 206, - 81, - 12, - 245, + 107, + 225, + 59, + 220, + 250, + 19, + 170, + 7, + 136, + 103, 158, - 29, - 28, - 107 + 53, + 102, + 152, + 76, + 243, + 34, + 47, + 12, + 39, + 184, + 73, + 220, + 222, + 7, + 184, + 237, + 172, + 40, + 150 ], "score": { "structural_complexity": 0.0, @@ -5570,48 +5570,48 @@ }, "./src/backend/transpiler/expressions_helpers/binary_ops.rs": { "content_hash": [ - 239, - 80, + 87, + 7, + 145, + 52, + 194, + 48, + 112, + 236, + 241, + 26, + 133, + 27, + 54, + 186, + 149, + 21, + 122, + 38, + 30, + 234, + 66, + 123, 182, - 245, - 227, - 243, - 160, - 204, - 196, - 191, - 110, - 213, - 163, - 36, - 119, - 107, - 144, + 159, + 208, 203, - 130, - 108, - 68, - 33, - 139, - 168, - 141, - 242, - 43, - 158, - 220, - 107, - 244, - 181 + 129, + 246, + 121, + 85, + 210, + 180 ], "score": { "structural_complexity": 4.6, "semantic_complexity": 20.0, - "duplication_ratio": 16.989246, + "duplication_ratio": 16.93166, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 81.58925, + "total": 81.53166, "grade": "B+", "confidence": 1.0, "language": "Rust", @@ -5623,15 +5623,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 44 duplicate code patterns" + "issue": "Found 42 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.0107527, + "amount": 3.0683403, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.1%" + "issue": "Code duplication: 15.3%" }, { "source_metric": "StructuralComplexity", @@ -5665,38 +5665,38 @@ }, "./src/backend/transpiler/expressions_helpers/collections.rs": { "content_hash": [ - 87, - 155, - 50, - 220, - 212, - 55, - 179, - 154, - 185, - 185, - 232, - 83, - 219, - 194, - 91, - 237, - 20, - 133, - 185, - 229, - 80, - 12, + 236, + 209, 63, + 81, 30, - 60, + 35, + 151, + 150, + 10, + 139, + 175, + 7, + 62, 137, - 182, - 113, - 161, - 161, - 58, - 0 + 47, + 231, + 179, + 63, + 94, + 132, + 124, + 23, + 248, + 195, + 179, + 22, + 255, + 49, + 122, + 194, + 198, + 215 ], "score": { "structural_complexity": 16.9, @@ -6091,48 +6091,48 @@ }, "./src/backend/transpiler/function_signature.rs": { "content_hash": [ - 217, - 87, - 242, - 178, - 187, - 154, - 105, - 134, - 100, - 39, - 8, - 245, - 119, - 59, - 71, - 105, - 179, - 87, - 167, - 158, - 186, - 189, - 85, - 231, - 237, - 227, - 87, - 74, + 216, + 12, + 223, + 11, 159, - 200, - 165, - 140 + 233, + 88, + 246, + 253, + 215, + 33, + 95, + 152, + 73, + 218, + 116, + 72, + 94, + 5, + 169, + 79, + 75, + 147, + 46, + 48, + 174, + 222, + 141, + 19, + 137, + 160, + 213 ], "score": { - "structural_complexity": 0.0, + "structural_complexity": 0.09999943, "semantic_complexity": 20.0, - "duplication_ratio": 16.813318, + "duplication_ratio": 16.774971, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.81332, + "total": 76.87497, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -6156,27 +6156,27 @@ }, { "source_metric": "Duplication", - "amount": 3.1866825, + "amount": 3.22503, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.9%" + "issue": "Code duplication: 16.1%" }, { "source_metric": "StructuralComplexity", - "amount": 10.0, + "amount": 9.900001, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 50" + "issue": "High cognitive complexity: 48" }, { "source_metric": "StructuralComplexity", - "amount": 15.0, + "amount": 14.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 60" + "issue": "High cyclomatic complexity: 58" } ], "critical_defects_count": 0, @@ -6194,48 +6194,48 @@ }, "./src/backend/transpiler/function_transpiler.rs": { "content_hash": [ - 35, - 243, + 176, + 103, + 190, + 94, + 78, + 249, + 64, + 137, + 21, 247, - 98, - 244, - 18, - 194, - 248, - 49, - 31, - 1, - 114, - 189, - 200, - 68, - 228, + 177, + 61, + 254, + 26, + 41, + 179, + 15, + 142, 147, - 129, - 16, - 150, - 100, - 145, - 229, - 38, - 247, - 47, - 224, - 189, - 234, - 71, - 99, - 120 + 165, + 68, + 57, + 254, + 41, + 42, + 253, + 4, + 78, + 112, + 40, + 40, + 157 ], "score": { - "structural_complexity": 18.7, + "structural_complexity": 19.6, "semantic_complexity": 20.0, - "duplication_ratio": 14.62963, + "duplication_ratio": 14.026667, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.329636, + "total": 93.62666, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -6247,23 +6247,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 29 duplicate code patterns" + "issue": "Found 22 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.37037, + "amount": 5.9733334, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 26.9%" + "issue": "Code duplication: 29.9%" }, { "source_metric": "StructuralComplexity", - "amount": 6.3, + "amount": 5.4, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 36" + "issue": "High cognitive complexity: 33" } ], "critical_defects_count": 0, @@ -7750,48 +7750,48 @@ }, "./src/backend/transpiler/program_transpiler.rs": { "content_hash": [ - 227, - 183, - 19, - 21, - 187, - 175, - 246, - 135, - 174, - 123, - 58, - 123, - 51, - 231, - 139, - 54, - 27, - 93, - 143, - 78, - 228, - 251, - 134, - 33, - 187, - 17, - 18, - 55, - 20, - 46, - 66, - 33 + 160, + 209, + 255, + 12, + 153, + 198, + 132, + 226, + 142, + 128, + 150, + 178, + 67, + 95, + 86, + 186, + 103, + 154, + 1, + 52, + 145, + 103, + 180, + 212, + 151, + 26, + 192, + 68, + 223, + 80, + 38, + 223 ], "score": { - "structural_complexity": 9.1, + "structural_complexity": 9.0, "semantic_complexity": 20.0, - "duplication_ratio": 12.883959, + "duplication_ratio": 12.835052, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 81.98396, + "total": 81.83505, "grade": "B+", "confidence": 1.0, "language": "Rust", @@ -7803,31 +7803,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 124 duplicate code patterns" + "issue": "Found 122 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 7.116041, + "amount": 7.1649485, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 35.6%" + "issue": "Code duplication: 35.8%" }, { "source_metric": "StructuralComplexity", - "amount": 6.9, + "amount": 7.5000005, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 38" + "issue": "High cognitive complexity: 40" }, { "source_metric": "StructuralComplexity", - "amount": 9.0, + "amount": 8.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 48" + "issue": "High cyclomatic complexity: 47" } ], "critical_defects_count": 0, @@ -7924,38 +7924,38 @@ }, "./src/backend/transpiler/return_type_helpers.rs": { "content_hash": [ - 255, - 92, - 236, - 189, - 185, - 193, - 156, + 145, + 175, + 214, + 40, 143, - 118, - 110, + 55, + 61, + 180, + 47, + 69, + 184, + 24, + 53, + 214, + 244, + 88, 58, - 206, - 109, - 200, - 104, - 20, - 207, - 240, - 60, - 93, - 8, + 132, + 119, 237, - 38, - 54, + 51, + 26, + 118, + 27, + 8, + 45, 109, - 188, - 63, - 128, - 9, - 197, - 101, - 119 + 29, + 40, + 107, + 185, + 31 ], "score": { "structural_complexity": 0.0, @@ -8802,48 +8802,48 @@ }, "./src/backend/transpiler/types.rs": { "content_hash": [ - 43, - 6, - 237, - 170, - 101, - 218, - 2, - 58, - 102, - 76, - 212, - 136, - 54, - 191, - 216, - 77, - 136, - 52, - 209, + 217, + 10, + 232, + 214, + 75, + 78, + 9, + 111, 254, - 210, - 56, - 1, - 77, - 73, - 0, - 72, - 58, - 67, - 28, - 238, - 33 + 146, + 105, + 170, + 154, + 239, + 103, + 186, + 48, + 30, + 232, + 143, + 68, + 66, + 127, + 159, + 118, + 243, + 94, + 140, + 8, + 104, + 153, + 248 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.461473, + "duplication_ratio": 15.392405, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.46147, + "total": 75.3924, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -8863,15 +8863,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 135 duplicate code patterns" + "issue": "Found 137 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.5385265, + "amount": 4.607595, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 22.7%" + "issue": "Code duplication: 23.0%" }, { "source_metric": "StructuralComplexity", @@ -8879,7 +8879,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 153" + "issue": "High cognitive complexity: 157" }, { "source_metric": "StructuralComplexity", @@ -8887,7 +8887,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 132" + "issue": "High cyclomatic complexity: 130" } ], "critical_defects_count": 0, @@ -9711,38 +9711,38 @@ }, "./src/bench/mod.rs": { "content_hash": [ - 192, - 56, - 75, - 33, - 13, - 125, - 0, - 241, - 121, - 241, - 63, - 132, - 190, - 154, - 62, - 102, - 203, - 169, - 126, + 105, 149, - 250, - 77, - 12, - 213, - 224, - 77, - 64, - 145, - 112, - 68, + 47, + 133, + 138, + 84, + 106, + 214, + 115, + 148, + 164, + 65, + 87, + 124, 101, - 45 + 198, + 165, + 17, + 147, + 121, + 163, + 51, + 52, + 203, + 94, + 123, + 134, + 1, + 106, + 143, + 62, + 174 ], "score": { "structural_complexity": 25.0, @@ -10454,48 +10454,48 @@ }, "./src/bin/handlers/command_router.rs": { "content_hash": [ - 85, - 10, - 207, - 121, - 119, - 104, - 93, + 188, + 7, + 223, + 224, + 33, + 164, + 2, + 91, + 61, + 174, + 99, + 138, 253, - 27, - 136, - 140, - 68, - 48, + 4, 18, - 242, - 163, - 177, - 189, - 110, - 162, - 80, + 96, + 62, + 69, + 79, + 64, + 198, + 240, + 75, + 239, 52, - 147, - 160, - 122, - 34, - 94, - 159, - 114, - 162, - 94, - 174 + 57, + 245, + 219, + 209, + 98, + 154, + 153 ], "score": { "structural_complexity": 8.2, "semantic_complexity": 20.0, - "duplication_ratio": 17.794872, + "duplication_ratio": 17.793493, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 85.99487, + "total": 85.99349, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -10507,11 +10507,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 130 duplicate code patterns" + "issue": "Found 121 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.2051282, + "amount": 2.2065065, "applied_to": [ "Duplication" ], @@ -10531,7 +10531,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 67" + "issue": "High cyclomatic complexity: 66" } ], "critical_defects_count": 0, @@ -11727,49 +11727,49 @@ }, "./src/bin/handlers/handlers_modules/sovereign.rs": { "content_hash": [ - 244, - 107, - 68, - 82, - 187, - 169, - 253, - 106, - 175, - 41, - 96, - 42, - 49, - 46, - 11, - 187, - 91, - 25, - 47, - 160, - 225, - 41, - 189, - 91, - 61, - 161, - 63, + 112, + 104, + 249, + 213, + 99, 193, + 94, + 172, + 199, + 27, + 118, + 25, + 131, + 75, + 71, + 178, + 199, + 134, + 59, + 57, + 93, + 190, 84, - 206, + 212, + 50, + 235, + 85, + 48, + 3, + 133, 70, - 86 + 42 ], "score": { - "structural_complexity": 2.5, + "structural_complexity": 3.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.13355, + "duplication_ratio": 17.069923, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 79.63355, - "grade": "B", + "total": 80.06992, + "grade": "B+", "confidence": 1.0, "language": "Rust", "file_path": "./src/bin/handlers/handlers_modules/sovereign.rs", @@ -11784,11 +11784,11 @@ }, { "source_metric": "Duplication", - "amount": 2.8664496, + "amount": 2.9300776, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 14.3%" + "issue": "Code duplication: 14.7%" }, { "source_metric": "StructuralComplexity", @@ -11796,15 +11796,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 63" + "issue": "High cognitive complexity: 62" }, { "source_metric": "StructuralComplexity", - "amount": 12.5, + "amount": 12.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 55" + "issue": "High cyclomatic complexity: 54" } ], "critical_defects_count": 0, @@ -11901,48 +11901,48 @@ }, "./src/bin/handlers/mcp_handler.rs": { "content_hash": [ - 35, - 53, - 27, - 13, - 127, - 235, - 244, + 204, + 28, + 67, + 219, + 46, + 151, + 221, + 126, + 78, + 198, + 241, + 92, + 37, + 140, 172, - 5, + 86, + 122, + 229, + 38, + 241, + 31, 60, - 206, - 134, - 18, - 190, - 185, - 27, - 175, - 141, - 183, - 82, - 145, - 63, - 85, - 153, - 45, - 111, - 154, - 17, - 32, + 179, + 143, + 100, + 237, + 124, 140, - 150, - 245 + 214, + 61, + 177, + 91 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 19.0, - "duplication_ratio": 16.774193, + "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 91.61291, + "total": 94.545456, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -11954,15 +11954,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 14 duplicate code patterns" - }, - { - "source_metric": "Duplication", - "amount": 3.2258062, - "applied_to": [ - "Duplication" - ], - "issue": "Code duplication: 16.1%" + "issue": "Found 19 duplicate code patterns" }, { "source_metric": "SemanticComplexity", @@ -12905,38 +12897,38 @@ }, "./src/bin/handlers/rhl_handler.rs": { "content_hash": [ - 44, - 34, - 166, - 122, - 81, + 31, + 177, + 224, + 170, + 148, + 1, + 43, + 252, + 160, 28, - 213, - 251, - 166, - 202, + 87, + 217, + 220, + 140, + 144, + 219, + 2, + 183, + 250, + 141, + 82, 50, - 182, - 158, - 35, - 239, - 34, - 16, + 71, + 188, + 124, + 235, + 45, + 159, + 105, + 58, 111, - 113, - 95, - 179, - 145, - 7, - 189, - 38, - 82, - 116, - 232, - 174, - 168, - 10, - 140 + 83 ], "score": { "structural_complexity": 25.0, @@ -12945,8 +12937,8 @@ "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, - "entropy_score": 7.0, - "total": 97.27273, + "entropy_score": 8.5, + "total": 98.63637, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -12954,11 +12946,11 @@ "penalties_applied": [ { "source_metric": "Duplication", - "amount": 3.0, + "amount": 1.5, "applied_to": [ "Duplication" ], - "issue": "Found 6 duplicate code patterns" + "issue": "Found 3 duplicate code patterns" } ], "critical_defects_count": 0, @@ -13055,48 +13047,48 @@ }, "./src/bin/handlers/serve_handler.rs": { "content_hash": [ - 205, - 39, - 84, - 244, - 204, - 62, - 6, - 48, - 248, - 64, - 137, - 101, - 80, - 70, - 133, 59, - 89, - 76, - 17, - 143, - 188, - 197, - 195, - 69, - 112, - 197, + 166, + 176, + 38, + 125, + 73, + 174, 173, - 18, - 65, - 124, - 49, - 197 + 132, + 25, + 135, + 55, + 200, + 136, + 205, + 241, + 111, + 23, + 235, + 186, + 2, + 44, + 113, + 81, + 226, + 147, + 252, + 44, + 146, + 149, + 73, + 99 ], "score": { - "structural_complexity": 19.3, + "structural_complexity": 19.9, "semantic_complexity": 17.5, - "duplication_ratio": 15.62982, + "duplication_ratio": 16.007393, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.42982, + "total": 93.407394, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -13108,23 +13100,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 89 duplicate code patterns" + "issue": "Found 70 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.37018, + "amount": 3.9926064, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 21.9%" + "issue": "Code duplication: 20.0%" }, { "source_metric": "StructuralComplexity", - "amount": 5.7000003, + "amount": 5.1000004, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 34" + "issue": "High cognitive complexity: 32" }, { "source_metric": "SemanticComplexity", @@ -13490,48 +13482,48 @@ }, "./src/bin/ruchy.rs": { "content_hash": [ - 89, - 161, - 63, - 111, - 200, - 58, - 172, - 166, - 210, - 141, - 242, - 228, - 51, - 40, + 243, + 156, 49, + 252, + 14, + 254, + 123, + 238, + 150, + 58, + 192, + 97, + 197, + 64, + 171, + 3, + 91, + 2, + 80, + 178, + 223, + 235, 45, - 120, - 65, - 32, - 37, - 209, - 143, - 214, - 209, - 15, - 147, - 85, - 233, - 21, - 1, - 149, - 239 + 74, + 108, + 131, + 27, + 215, + 64, + 35, + 93, + 161 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 18.0, - "duplication_ratio": 14.078947, + "duplication_ratio": 14.036585, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 72.07895, + "total": 72.03658, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -13543,15 +13535,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 173 duplicate code patterns" + "issue": "Found 168 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.921053, + "amount": 5.963415, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.6%" + "issue": "Code duplication: 29.8%" }, { "source_metric": "StructuralComplexity", @@ -13559,7 +13551,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 80" + "issue": "High cognitive complexity: 78" }, { "source_metric": "StructuralComplexity", @@ -13567,7 +13559,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 106" + "issue": "High cyclomatic complexity: 101" }, { "source_metric": "SemanticComplexity", @@ -14637,48 +14629,48 @@ }, "./src/frontend/ast.rs": { "content_hash": [ - 82, - 27, - 236, - 8, - 96, - 198, - 228, - 181, - 46, - 158, - 48, - 251, - 67, - 240, - 27, + 76, + 135, + 195, + 247, 86, - 133, - 239, - 87, - 252, - 43, - 85, - 30, - 22, - 237, - 144, - 136, - 187, + 171, + 33, + 130, + 153, + 29, + 244, + 186, + 207, + 108, + 53, + 138, + 223, + 196, + 46, + 127, + 10, + 69, 147, - 97, - 195, - 202 + 157, + 228, + 229, + 127, + 57, + 96, + 191, + 135, + 243 ], "score": { "structural_complexity": 12.5, "semantic_complexity": 20.0, - "duplication_ratio": 15.500516, + "duplication_ratio": 15.49325, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 88.00052, + "total": 87.99325, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -14694,7 +14686,7 @@ }, { "source_metric": "Duplication", - "amount": 4.499484, + "amount": 4.5067496, "applied_to": [ "Duplication" ], @@ -16779,38 +16771,38 @@ }, "./src/frontend/parser/expressions_helpers/modules.rs": { "content_hash": [ - 101, - 155, - 19, - 223, - 83, - 45, - 196, - 242, - 218, - 62, - 104, - 153, - 221, - 227, - 40, - 73, - 192, - 96, - 91, - 49, - 41, - 57, - 135, - 78, - 254, - 44, - 134, - 140, - 106, - 229, - 161, - 253 + 214, + 90, + 108, + 125, + 169, + 95, + 18, + 74, + 31, + 98, + 251, + 59, + 230, + 67, + 131, + 52, + 130, + 202, + 29, + 209, + 68, + 64, + 42, + 92, + 119, + 130, + 122, + 28, + 16, + 131, + 61, + 117 ], "score": { "structural_complexity": 25.0, @@ -16832,7 +16824,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 12 duplicate code patterns" + "issue": "Found 13 duplicate code patterns" } ], "critical_defects_count": 0, @@ -16850,48 +16842,48 @@ }, "./src/frontend/parser/expressions_helpers/patterns.rs": { "content_hash": [ - 54, - 111, - 107, - 78, - 69, - 199, - 40, - 161, - 55, + 32, + 91, + 24, + 179, 208, - 36, - 93, - 111, - 105, - 122, - 43, + 164, + 135, + 171, + 89, + 102, + 210, + 186, 68, - 79, - 43, - 178, - 238, - 237, - 182, - 215, - 26, - 122, - 136, - 1, - 19, - 153, + 186, + 44, + 108, + 217, + 246, + 228, + 137, 174, - 82 + 119, + 203, + 206, + 223, + 9, + 238, + 105, + 242, + 117, + 65, + 127 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 19.0, - "duplication_ratio": 16.0364, + "duplication_ratio": 16.032389, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.0364, + "total": 75.03239, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -16907,7 +16899,7 @@ }, { "source_metric": "Duplication", - "amount": 3.9635994, + "amount": 3.9676113, "applied_to": [ "Duplication" ], @@ -16927,7 +16919,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 172" + "issue": "High cyclomatic complexity: 171" }, { "source_metric": "SemanticComplexity", @@ -17760,48 +17752,48 @@ }, "./src/frontend/parser/functions.rs": { "content_hash": [ - 95, - 144, - 172, - 186, - 131, - 244, - 161, - 180, - 212, - 137, - 122, - 1, - 128, - 217, - 4, - 253, - 10, - 231, - 137, - 124, - 246, - 254, - 145, - 94, - 168, - 3, - 51, - 57, - 26, - 31, + 154, + 113, + 12, + 169, + 17, + 136, + 7, + 230, + 153, + 174, + 6, + 106, + 189, + 76, + 123, + 146, + 35, + 21, + 64, + 30, + 167, + 224, 186, - 250 + 75, + 247, + 173, + 208, + 6, + 102, + 154, + 123, + 17 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.897436, + "duplication_ratio": 15.78125, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.89744, + "total": 75.78125, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -17813,15 +17805,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 104 duplicate code patterns" + "issue": "Found 105 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.1025643, + "amount": 4.21875, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.5%" + "issue": "Code duplication: 21.1%" }, { "source_metric": "StructuralComplexity", @@ -18124,48 +18116,48 @@ }, "./src/frontend/parser/mod.rs": { "content_hash": [ - 116, - 45, - 232, - 201, - 218, - 233, - 237, - 227, - 182, - 70, - 124, - 38, - 250, - 207, - 92, - 155, - 251, - 134, - 55, - 134, - 132, - 27, - 154, - 209, + 247, + 213, + 22, 110, - 250, - 133, - 173, - 105, - 210, - 132, - 26 + 214, + 62, + 87, + 16, + 46, + 183, + 244, + 147, + 183, + 65, + 240, + 241, + 6, + 13, + 86, + 106, + 247, + 124, + 231, + 76, + 41, + 125, + 33, + 17, + 3, + 148, + 226, + 100 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.042398, + "duplication_ratio": 17.003122, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.0424, + "total": 77.00312, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -18177,15 +18169,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 67 duplicate code patterns" + "issue": "Found 68 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.957601, + "amount": 2.9968784, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 14.8%" + "issue": "Code duplication: 15.0%" }, { "source_metric": "StructuralComplexity", @@ -18890,38 +18882,38 @@ }, "./src/frontend/parser/utils_helpers/string_interpolation.rs": { "content_hash": [ - 177, - 26, - 145, - 48, - 114, - 26, + 197, + 187, 167, - 191, - 6, - 170, - 83, - 9, + 59, + 80, + 152, + 98, + 139, 19, - 115, - 73, - 102, - 159, - 127, - 28, + 150, + 177, + 233, + 108, + 205, + 21, + 56, + 198, + 56, + 130, + 41, + 94, + 97, + 158, + 195, + 146, + 80, 69, - 112, - 38, - 231, - 202, - 203, - 71, - 251, - 139, - 212, - 17, - 100, - 55 + 159, + 142, + 240, + 169, + 165 ], "score": { "structural_complexity": 20.3, @@ -20830,77 +20822,6 @@ }, "git_context": null }, - "./src/mcp/rhl_tools.rs": { - "content_hash": [ - 62, - 74, - 163, - 104, - 209, - 199, - 229, - 216, - 81, - 169, - 60, - 27, - 102, - 83, - 77, - 58, - 105, - 60, - 208, - 63, - 192, - 253, - 245, - 181, - 98, - 36, - 247, - 27, - 53, - 201, - 63, - 132 - ], - "score": { - "structural_complexity": 25.0, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 5.0, - "total": 95.454544, - "grade": "A+", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/mcp/rhl_tools.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 5.0, - "applied_to": [ - "Duplication" - ], - "issue": "Found 11 duplicate code patterns" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, "./src/mcp/tool_discovery.rs": { "content_hash": [ 250, @@ -20990,38 +20911,38 @@ }, "./src/mcp.rs": { "content_hash": [ - 135, - 215, - 251, - 50, - 22, - 194, - 19, - 5, - 131, - 166, - 84, - 66, - 47, - 140, - 46, - 172, - 154, - 66, - 86, - 252, - 150, - 24, - 179, - 78, - 49, - 36, - 251, 217, - 103, - 78, - 191, - 119 + 224, + 220, + 85, + 184, + 30, + 184, + 4, + 49, + 82, + 59, + 199, + 200, + 249, + 190, + 79, + 96, + 156, + 253, + 138, + 68, + 107, + 90, + 11, + 19, + 253, + 137, + 3, + 169, + 75, + 151, + 43 ], "score": { "structural_complexity": 25.0, @@ -21043,7 +20964,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 34 duplicate code patterns" + "issue": "Found 36 duplicate code patterns" } ], "critical_defects_count": 0, @@ -27752,38 +27673,38 @@ }, "./src/quality/formatter_config.rs": { "content_hash": [ - 252, + 224, + 145, + 198, + 147, + 201, + 131, + 129, + 58, + 33, + 72, + 6, + 105, + 187, + 211, + 56, + 98, + 91, + 93, + 52, 179, - 135, - 66, + 42, + 218, + 179, + 106, + 123, 2, - 52, - 175, - 172, - 130, - 156, - 160, - 5, - 189, - 83, - 150, - 62, - 149, - 30, - 246, - 127, - 255, - 54, - 121, - 141, - 232, - 214, - 74, - 205, - 104, - 152, - 38, - 38 + 81, + 163, + 210, + 142, + 244, + 95 ], "score": { "structural_complexity": 25.0, @@ -29981,38 +29902,38 @@ }, "./src/rhl/check.rs": { "content_hash": [ - 68, - 228, - 168, - 155, - 232, - 157, - 114, - 168, + 207, + 62, + 191, 87, - 233, - 1, - 56, - 46, - 58, - 31, - 178, - 39, - 205, + 206, + 124, + 52, + 230, + 81, 34, - 41, - 153, - 170, - 246, - 13, - 33, - 110, - 179, - 209, + 147, + 3, + 28, + 242, + 136, + 244, + 138, + 193, + 249, + 24, 196, - 155, - 185, - 42 + 38, + 200, + 253, + 9, + 104, + 69, + 78, + 219, + 35, + 57, + 23 ], "score": { "structural_complexity": 23.2, @@ -30034,7 +29955,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 14 duplicate code patterns" + "issue": "Found 10 duplicate code patterns" }, { "source_metric": "StructuralComplexity", @@ -30060,49 +29981,49 @@ }, "./src/rhl/cli.rs": { "content_hash": [ - 158, - 144, - 241, - 223, - 92, - 202, - 131, - 169, - 254, - 130, - 238, - 208, + 221, + 154, + 38, + 188, + 220, + 229, 139, - 218, - 88, - 118, - 204, - 179, - 253, - 73, + 110, + 31, + 207, + 54, + 220, + 148, + 45, + 51, + 137, + 145, + 48, + 240, + 78, 150, - 76, - 134, - 117, - 86, - 104, - 41, - 230, - 126, - 243, - 28, - 138 + 140, + 227, + 120, + 232, + 26, + 166, + 150, + 241, + 194, + 44, + 122 ], "score": { - "structural_complexity": 0.0, + "structural_complexity": 22.0, "semantic_complexity": 20.0, "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 80.0, - "grade": "B+", + "total": 92.72727, + "grade": "A", "confidence": 1.0, "language": "Rust", "file_path": "./src/rhl/cli.rs", @@ -30113,23 +30034,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 47 duplicate code patterns" + "issue": "Found 21 duplicate code patterns" }, { "source_metric": "StructuralComplexity", - "amount": 10.0, + "amount": 1.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 50" + "issue": "High cognitive complexity: 20" }, { "source_metric": "StructuralComplexity", - "amount": 15.0, + "amount": 1.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 81" + "issue": "High cyclomatic complexity: 33" } ], "critical_defects_count": 0, @@ -30147,38 +30068,38 @@ }, "./src/rhl/codes.rs": { "content_hash": [ - 35, 238, - 184, - 51, - 254, - 25, - 147, + 251, + 57, + 42, + 212, + 151, + 243, + 88, + 2, + 69, + 155, + 241, + 135, 74, - 218, - 47, - 68, - 186, + 88, 163, - 80, - 141, - 159, - 116, - 48, - 242, - 192, - 143, - 165, - 238, - 96, - 118, + 127, + 196, + 191, + 6, 7, - 144, - 164, - 250, + 36, + 33, + 237, + 42, + 218, + 255, + 111, + 107, + 198, 231, - 8, - 158 + 55 ], "score": { "structural_complexity": 25.0, @@ -30187,8 +30108,8 @@ "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, - "entropy_score": 7.5, - "total": 97.72727, + "entropy_score": 8.5, + "total": 98.63637, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -30196,169 +30117,11 @@ "penalties_applied": [ { "source_metric": "Duplication", - "amount": 2.5, - "applied_to": [ - "Duplication" - ], - "issue": "Found 5 duplicate code patterns" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, - "./src/rhl/contract.rs": { - "content_hash": [ - 158, - 135, - 143, - 79, - 221, - 80, - 51, - 214, - 62, - 212, - 215, - 105, - 91, - 243, - 115, - 29, - 17, - 183, - 233, - 246, - 159, - 29, - 97, - 124, - 30, - 123, - 162, - 116, - 179, - 112, - 59, - 224 - ], - "score": { - "structural_complexity": 6.1, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 5.0, - "total": 86.1, - "grade": "A-", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/rhl/contract.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 5.0, - "applied_to": [ - "Duplication" - ], - "issue": "Found 17 duplicate code patterns" - }, - { - "source_metric": "StructuralComplexity", - "amount": 3.9, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cognitive complexity: 28" - }, - { - "source_metric": "StructuralComplexity", - "amount": 15.0, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cyclomatic complexity: 73" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, - "./src/rhl/contract_render.rs": { - "content_hash": [ - 179, - 1, - 7, - 12, - 3, - 171, - 37, - 125, - 142, - 171, - 242, - 39, - 50, - 37, - 254, - 249, - 202, - 124, - 161, - 95, - 131, - 229, - 207, - 36, - 113, - 221, - 106, - 226, - 147, - 181, - 69, - 56 - ], - "score": { - "structural_complexity": 25.0, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 5.0, - "total": 95.454544, - "grade": "A+", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/rhl/contract_render.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 5.0, + "amount": 1.5, "applied_to": [ "Duplication" ], - "issue": "Found 14 duplicate code patterns" + "issue": "Found 3 duplicate code patterns" } ], "critical_defects_count": 0, @@ -30445,180 +30208,6 @@ }, "git_context": null }, - "./src/rhl/examples.rs": { - "content_hash": [ - 73, - 69, - 222, - 222, - 221, - 232, - 105, - 129, - 217, - 154, - 126, - 150, - 228, - 130, - 244, - 209, - 166, - 96, - 75, - 86, - 57, - 174, - 137, - 196, - 74, - 138, - 106, - 132, - 41, - 23, - 212, - 157 - ], - "score": { - "structural_complexity": 12.0, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 5.0, - "total": 92.0, - "grade": "A", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/rhl/examples.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 5.0, - "applied_to": [ - "Duplication" - ], - "issue": "Found 27 duplicate code patterns" - }, - { - "source_metric": "StructuralComplexity", - "amount": 6.0, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cognitive complexity: 35" - }, - { - "source_metric": "StructuralComplexity", - "amount": 7.0, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cyclomatic complexity: 44" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, - "./src/rhl/explain.rs": { - "content_hash": [ - 221, - 48, - 84, - 65, - 152, - 112, - 60, - 137, - 143, - 205, - 1, - 10, - 7, - 66, - 108, - 56, - 24, - 120, - 232, - 51, - 166, - 100, - 157, - 140, - 134, - 251, - 193, - 240, - 103, - 166, - 110, - 186 - ], - "score": { - "structural_complexity": 13.0, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 5.0, - "total": 93.0, - "grade": "A", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/rhl/explain.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 5.0, - "applied_to": [ - "Duplication" - ], - "issue": "Found 15 duplicate code patterns" - }, - { - "source_metric": "StructuralComplexity", - "amount": 3.0, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cognitive complexity: 25" - }, - { - "source_metric": "StructuralComplexity", - "amount": 9.0, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cyclomatic complexity: 48" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, "./src/rhl/fix.rs": { "content_hash": [ 217, @@ -30700,38 +30289,38 @@ }, "./src/rhl/fmt.rs": { "content_hash": [ - 100, - 243, - 222, - 164, - 145, - 162, - 65, - 130, - 234, - 76, - 243, - 92, - 144, - 169, - 139, - 120, - 95, - 159, - 21, - 177, - 218, - 253, - 82, - 181, - 92, - 198, - 73, - 200, 47, - 206, - 69, - 81 + 28, + 196, + 39, + 208, + 229, + 187, + 99, + 9, + 228, + 30, + 220, + 163, + 169, + 141, + 4, + 171, + 202, + 98, + 201, + 234, + 58, + 228, + 52, + 179, + 25, + 5, + 17, + 155, + 179, + 133, + 181 ], "score": { "structural_complexity": 8.1, @@ -30785,127 +30374,40 @@ }, "git_context": null }, - "./src/rhl/lower.rs": { - "content_hash": [ - 166, - 116, - 206, - 120, - 84, - 21, - 98, - 230, - 31, - 237, - 120, - 12, - 220, - 55, - 90, - 243, - 50, - 241, - 208, - 110, - 109, - 91, - 53, - 46, - 139, - 230, - 123, - 218, - 244, - 74, - 39, - 225 - ], - "score": { - "structural_complexity": 0.0, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 5.0, - "total": 80.0, - "grade": "B+", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/rhl/lower.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 5.0, - "applied_to": [ - "Duplication" - ], - "issue": "Found 77 duplicate code patterns" - }, - { - "source_metric": "StructuralComplexity", - "amount": 10.0, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cognitive complexity: 88" - }, - { - "source_metric": "StructuralComplexity", - "amount": 15.0, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cyclomatic complexity: 165" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, "./src/rhl/mod.rs": { "content_hash": [ - 12, - 40, + 23, + 15, 19, + 160, + 211, + 32, + 74, + 201, + 109, + 242, + 195, + 158, + 166, + 208, + 150, + 240, + 15, + 78, + 179, + 152, + 215, + 243, + 24, + 226, 87, - 218, - 63, - 156, - 173, - 151, - 60, - 47, - 43, - 119, - 182, - 34, - 237, - 237, - 108, - 186, - 204, - 89, + 81, + 129, + 124, + 23, + 165, 172, - 240, - 9, - 238, - 26, - 133, - 45, - 111, - 253, - 229, - 250 + 255 ], "score": { "structural_complexity": 25.0, @@ -31014,148 +30516,6 @@ }, "git_context": null }, - "./src/rhl/receipt.rs": { - "content_hash": [ - 120, - 79, - 105, - 100, - 204, - 149, - 56, - 255, - 253, - 141, - 254, - 121, - 104, - 126, - 86, - 180, - 168, - 81, - 209, - 91, - 182, - 60, - 145, - 85, - 225, - 62, - 178, - 34, - 139, - 115, - 188, - 48 - ], - "score": { - "structural_complexity": 25.0, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 6.0, - "total": 96.36363, - "grade": "A+", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/rhl/receipt.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 4.0, - "applied_to": [ - "Duplication" - ], - "issue": "Found 8 duplicate code patterns" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, - "./src/rhl/runtime.rs": { - "content_hash": [ - 213, - 200, - 189, - 210, - 222, - 185, - 111, - 81, - 220, - 193, - 67, - 174, - 236, - 158, - 220, - 0, - 200, - 18, - 195, - 187, - 215, - 66, - 179, - 111, - 158, - 31, - 18, - 170, - 9, - 189, - 121, - 138 - ], - "score": { - "structural_complexity": 25.0, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 5.0, - "total": 95.454544, - "grade": "A+", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/rhl/runtime.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 5.0, - "applied_to": [ - "Duplication" - ], - "issue": "Found 11 duplicate code patterns" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, "./src/rhl/tree.rs": { "content_hash": [ 0, @@ -31237,38 +30597,38 @@ }, "./src/rhl/vocab.rs": { "content_hash": [ - 30, - 231, - 136, - 64, - 176, - 55, - 65, - 151, - 117, - 198, - 230, - 246, - 236, - 130, - 142, - 201, - 212, - 174, - 17, - 106, - 76, - 204, + 82, + 157, + 218, + 111, + 150, + 90, + 240, + 213, + 75, + 108, + 129, + 222, + 235, + 215, + 164, + 239, + 158, + 102, 176, - 148, - 173, - 32, - 60, + 103, + 2, + 192, + 203, + 153, + 188, + 235, + 203, + 133, + 180, 53, - 74, - 130, - 101, - 8 + 44, + 136 ], "score": { "structural_complexity": 25.0, @@ -31308,38 +30668,38 @@ }, "./src/rhl/vocab_cli.rs": { "content_hash": [ - 154, - 245, - 32, - 137, - 20, - 12, - 46, - 253, - 203, - 22, - 130, - 213, - 75, - 98, + 150, + 136, + 0, + 80, + 6, + 93, + 194, + 220, + 236, + 229, 120, - 151, - 58, - 82, - 125, - 247, - 90, + 218, + 255, + 60, + 41, + 166, 251, - 195, - 108, - 209, - 105, - 140, - 31, - 154, - 180, - 203, - 224 + 176, + 79, + 58, + 234, + 54, + 124, + 164, + 255, + 232, + 102, + 67, + 21, + 104, + 139, + 23 ], "score": { "structural_complexity": 25.0, @@ -31361,7 +30721,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 21 duplicate code patterns" + "issue": "Found 20 duplicate code patterns" } ], "critical_defects_count": 0, @@ -33616,48 +32976,48 @@ }, "./src/runtime/eval_array.rs": { "content_hash": [ - 141, - 7, - 80, - 58, - 143, - 74, - 93, - 159, - 53, - 141, - 23, - 160, + 99, + 216, 180, - 20, - 253, - 248, - 96, - 105, - 227, - 138, + 159, 241, - 239, - 242, + 4, + 181, + 167, + 2, + 215, + 163, + 223, + 243, + 2, + 208, + 248, + 13, + 66, + 245, + 177, + 207, 242, - 158, - 117, - 61, - 10, - 148, - 114, - 229, - 184 + 175, + 173, + 241, + 156, + 110, + 252, + 106, + 65, + 167, + 117 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.8795185, + "duplication_ratio": 15.015873, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.87952, + "total": 75.01587, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33669,15 +33029,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 86 duplicate code patterns" + "issue": "Found 71 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.120482, + "amount": 4.984127, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.6%" + "issue": "Code duplication: 24.9%" }, { "source_metric": "StructuralComplexity", @@ -33685,7 +33045,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 104" + "issue": "High cognitive complexity: 100" }, { "source_metric": "StructuralComplexity", @@ -33693,7 +33053,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 143" + "issue": "High cyclomatic complexity: 131" } ], "critical_defects_count": 0, @@ -33711,38 +33071,38 @@ }, "./src/runtime/eval_builtin.rs": { "content_hash": [ - 173, - 139, - 202, + 144, + 156, + 156, + 68, 138, - 59, - 53, - 219, - 250, - 210, - 152, + 196, + 168, + 241, + 181, 134, - 27, - 248, - 25, - 204, - 11, - 178, - 224, - 255, - 24, - 4, - 165, - 250, 115, - 251, - 84, - 149, - 77, - 26, - 213, - 25, - 115 + 188, + 72, + 104, + 97, + 236, + 74, + 201, + 234, + 206, + 14, + 252, + 225, + 24, + 91, + 73, + 19, + 189, + 104, + 44, + 78, + 183 ], "score": { "structural_complexity": 0.0, @@ -33806,48 +33166,48 @@ }, "./src/runtime/eval_builtin_fs.rs": { "content_hash": [ - 163, - 130, - 120, - 222, - 168, - 53, - 74, - 214, - 145, - 154, - 187, - 239, - 71, - 245, + 134, + 158, + 181, + 66, 252, - 175, - 132, - 111, + 0, + 50, + 254, + 218, + 173, + 29, + 127, + 247, + 169, + 127, + 152, + 204, + 89, 96, - 156, - 196, - 158, - 94, - 79, - 162, - 42, - 205, - 107, - 216, - 60, - 65, - 128 + 188, + 166, + 254, + 113, + 199, + 152, + 22, + 194, + 210, + 224, + 48, + 253, + 98 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.007385, + "duplication_ratio": 15.029411, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.007385, + "total": 75.02941, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33863,11 +33223,11 @@ }, { "source_metric": "Duplication", - "amount": 4.9926143, + "amount": 4.970588, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 25.0%" + "issue": "Code duplication: 24.9%" }, { "source_metric": "StructuralComplexity", @@ -35668,47 +35028,47 @@ "./src/runtime/eval_operations.rs": { "content_hash": [ 145, - 124, - 227, - 136, - 252, - 51, - 231, - 54, - 93, - 78, - 231, - 99, - 44, - 218, - 82, - 27, - 147, - 48, - 54, - 189, - 218, - 183, - 57, - 254, + 0, + 130, + 59, 64, - 192, 17, - 193, - 184, - 34, + 240, + 78, + 127, + 180, + 3, + 149, + 135, + 213, + 130, + 194, + 61, 174, - 16 + 167, + 104, + 241, + 145, + 146, + 68, + 3, + 37, + 150, + 167, + 227, + 96, + 172, + 117 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.779817, + "duplication_ratio": 15.770863, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.779816, + "total": 75.77086, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -35724,7 +35084,7 @@ }, { "source_metric": "Duplication", - "amount": 4.2201834, + "amount": 4.2291374, "applied_to": [ "Duplication" ], @@ -35744,7 +35104,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 149" + "issue": "High cyclomatic complexity: 148" } ], "critical_defects_count": 0, @@ -36142,38 +35502,38 @@ }, "./src/runtime/eval_string_methods.rs": { "content_hash": [ - 9, - 43, - 96, - 42, - 216, - 156, - 22, - 209, - 157, + 127, + 5, + 254, + 134, + 19, + 214, + 176, + 86, + 48, + 184, + 115, + 187, + 179, + 171, + 40, + 222, + 19, + 112, + 110, + 120, + 240, + 124, + 249, + 121, + 231, + 91, + 54, + 158, + 198, 94, - 10, - 203, - 42, - 82, - 162, - 114, - 157, - 165, - 106, - 204, - 226, - 195, - 117, - 38, - 216, - 66, - 122, - 206, - 82, - 76, - 142, - 180 + 140, + 70 ], "score": { "structural_complexity": 0.0, @@ -36791,38 +36151,38 @@ }, "./src/runtime/interpreter_control_flow.rs": { "content_hash": [ - 170, - 25, - 175, - 106, - 52, - 210, - 0, - 147, - 159, - 151, - 233, - 22, - 211, - 163, - 242, - 127, + 254, + 239, + 119, + 191, + 31, + 73, + 217, + 197, + 249, + 154, 192, - 198, - 247, - 108, - 13, - 199, + 5, + 67, + 135, + 238, + 16, + 214, + 102, + 161, + 127, + 23, 96, - 130, - 147, - 51, - 229, - 114, - 47, - 205, - 12, - 94 + 184, + 204, + 139, + 42, + 117, + 164, + 69, + 28, + 82, + 202 ], "score": { "structural_complexity": 0.0, @@ -36989,48 +36349,48 @@ }, "./src/runtime/interpreter_functions.rs": { "content_hash": [ - 82, - 198, - 254, - 164, - 239, - 157, - 93, - 221, - 146, - 7, - 102, - 248, - 7, - 166, - 232, - 103, - 155, - 246, - 141, - 100, - 208, - 129, - 216, - 179, - 203, - 132, - 215, + 119, + 76, + 12, + 224, + 8, 143, - 81, - 60, - 203, - 30 + 17, + 197, + 126, + 20, + 255, + 29, + 209, + 208, + 25, + 229, + 187, + 173, + 37, + 156, + 194, + 176, + 241, + 46, + 194, + 253, + 95, + 119, + 49, + 95, + 135, + 72 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.469526, + "duplication_ratio": 14.461182, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 74.46953, + "total": 74.46118, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -37050,11 +36410,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 103 duplicate code patterns" + "issue": "Found 98 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.530474, + "amount": 5.5388184, "applied_to": [ "Duplication" ], @@ -37066,15 +36426,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 116" + "issue": "High cognitive complexity: 114" }, { "source_metric": "StructuralComplexity", - "amount": 15.0, + "amount": 13.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 64" + "issue": "High cyclomatic complexity: 56" } ], "critical_defects_count": 0, @@ -37265,48 +36625,48 @@ }, "./src/runtime/interpreter_methods_actor.rs": { "content_hash": [ - 103, - 79, - 111, - 13, - 208, - 118, - 230, - 89, - 25, - 249, - 234, - 178, - 185, - 58, + 104, + 239, + 153, + 135, 143, + 185, + 32, + 184, + 218, + 13, + 140, + 71, + 199, + 17, + 237, + 162, + 14, + 239, 52, - 221, - 151, - 176, - 134, - 116, - 43, - 138, - 73, - 208, + 17, + 144, + 80, + 80, + 152, + 122, + 15, + 42, 188, - 224, - 7, - 117, - 147, + 252, 103, - 143 + 241, + 8 ], "score": { "structural_complexity": 9.5, "semantic_complexity": 20.0, - "duplication_ratio": 17.421053, + "duplication_ratio": 17.407408, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 86.92105, + "total": 86.90741, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -37322,11 +36682,11 @@ }, { "source_metric": "Duplication", - "amount": 2.5789473, + "amount": 2.5925925, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 12.9%" + "issue": "Code duplication: 13.0%" }, { "source_metric": "StructuralComplexity", @@ -37360,48 +36720,48 @@ }, "./src/runtime/interpreter_methods_dispatch.rs": { "content_hash": [ - 226, - 108, - 246, - 128, - 127, - 41, - 175, - 235, - 212, - 162, - 113, - 49, - 140, - 106, - 44, - 215, - 129, + 33, + 121, + 164, + 105, + 218, 76, + 252, + 192, + 108, + 57, + 234, + 53, + 119, + 244, + 4, + 56, + 6, + 219, + 224, + 102, + 217, + 105, + 255, + 194, + 61, + 16, + 205, 232, - 71, - 65, - 70, - 138, - 121, - 114, - 184, - 241, - 18, - 172, - 172, - 88, - 165 + 28, + 19, + 79, + 102 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.124374, + "duplication_ratio": 13.974066, "coupling_score": 11.4, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 70.524376, + "total": 70.37407, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -37421,15 +36781,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 179 duplicate code patterns" + "issue": "Found 175 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.8756256, + "amount": 6.025934, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.4%" + "issue": "Code duplication: 30.1%" }, { "source_metric": "StructuralComplexity", @@ -37437,7 +36797,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 117" + "issue": "High cognitive complexity: 122" }, { "source_metric": "StructuralComplexity", @@ -37445,7 +36805,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 70" + "issue": "High cyclomatic complexity: 68" }, { "source_metric": "Coupling", @@ -38096,48 +37456,48 @@ }, "./src/runtime/interpreter_types_class.rs": { "content_hash": [ - 150, - 140, - 146, - 6, - 5, - 240, + 224, + 22, 215, - 14, - 96, - 141, - 141, - 179, 161, - 214, - 197, - 108, - 247, - 244, - 52, - 65, - 247, - 105, - 241, - 37, - 232, - 111, - 7, - 189, - 98, - 132, - 20, - 107 + 40, + 155, + 41, + 177, + 103, + 249, + 171, + 145, + 0, + 139, + 235, + 254, + 43, + 17, + 68, + 56, + 222, + 229, + 15, + 137, + 246, + 216, + 178, + 46, + 149, + 112, + 236, + 127 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.882591, + "duplication_ratio": 16.80498, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.88259, + "total": 76.80498, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -38157,15 +37517,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 61 duplicate code patterns" + "issue": "Found 62 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.117409, + "amount": 3.195021, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.6%" + "issue": "Code duplication: 16.0%" }, { "source_metric": "StructuralComplexity", @@ -38173,7 +37533,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 177" + "issue": "High cognitive complexity: 181" }, { "source_metric": "StructuralComplexity", @@ -39456,48 +38816,48 @@ }, "./src/runtime/repl/evaluation.rs": { "content_hash": [ - 44, - 74, - 136, - 253, 83, - 233, - 166, - 23, - 138, - 77, - 131, + 32, + 118, + 98, + 37, + 80, + 57, + 87, 140, - 240, - 11, + 224, + 160, + 238, + 102, + 94, + 205, + 165, + 146, + 197, + 68, + 225, 241, - 222, - 173, - 3, - 179, - 82, - 202, - 80, - 142, - 179, - 245, - 227, - 56, - 47, - 210, - 70, - 199, - 85 + 205, + 138, + 89, + 77, + 34, + 205, + 102, + 30, + 33, + 157, + 136 ], "score": { - "structural_complexity": 0.39999962, + "structural_complexity": 1.2999992, "semantic_complexity": 20.0, - "duplication_ratio": 17.1537, + "duplication_ratio": 16.987951, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.553696, + "total": 78.28795, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -39513,19 +38873,19 @@ }, { "source_metric": "Duplication", - "amount": 2.8462996, + "amount": 3.0120482, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 14.2%" + "issue": "Code duplication: 15.1%" }, { "source_metric": "StructuralComplexity", - "amount": 9.6, + "amount": 8.700001, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 47" + "issue": "High cognitive complexity: 44" }, { "source_metric": "StructuralComplexity", @@ -39533,7 +38893,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 79" + "issue": "High cyclomatic complexity: 75" } ], "critical_defects_count": 0, @@ -40600,49 +39960,49 @@ }, "./src/runtime/value.rs": { "content_hash": [ - 43, - 11, + 230, + 96, + 88, + 3, 91, - 215, - 99, - 13, - 95, - 51, - 143, - 117, - 200, - 53, - 105, - 10, - 141, - 46, - 152, - 228, + 102, + 214, + 29, + 178, + 151, + 92, + 212, + 8, + 159, + 96, + 109, + 136, + 125, + 127, + 57, + 219, + 108, + 159, + 22, + 177, + 71, 91, - 40, - 126, - 185, - 220, - 244, - 188, - 126, - 162, - 208, - 93, - 237, - 226, - 176 + 248, + 42, + 205, + 250, + 210 ], "score": { - "structural_complexity": 19.5, + "structural_complexity": 21.5, "semantic_complexity": 20.0, "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 99.5, - "grade": "A+", + "total": 92.27273, + "grade": "A", "confidence": 1.0, "language": "Rust", "file_path": "./src/runtime/value.rs", @@ -40653,15 +40013,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 15 duplicate code patterns" + "issue": "Found 12 duplicate code patterns" }, { "source_metric": "StructuralComplexity", - "amount": 5.5, + "amount": 3.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 41" + "issue": "High cyclomatic complexity: 37" } ], "critical_defects_count": 0, @@ -40940,48 +40300,48 @@ }, "./src/server/watcher.rs": { "content_hash": [ - 135, - 46, - 184, - 173, - 85, - 74, - 246, - 78, - 55, - 160, - 176, - 191, - 16, - 229, - 159, - 218, - 51, - 104, - 219, - 178, - 63, - 160, + 128, + 138, + 162, + 107, 156, - 219, - 116, - 200, - 152, - 186, - 205, - 177, - 60, - 187 + 92, + 30, + 145, + 190, + 93, + 114, + 66, + 16, + 112, + 1, + 151, + 174, + 192, + 162, + 105, + 169, + 121, + 231, + 105, + 44, + 157, + 151, + 43, + 187, + 198, + 24, + 114 ], "score": { - "structural_complexity": 24.7, + "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.41573, + "duplication_ratio": 16.556292, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.83248, + "total": 92.3239, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -40997,19 +40357,11 @@ }, { "source_metric": "Duplication", - "amount": 2.5842695, + "amount": 3.4437087, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 12.9%" - }, - { - "source_metric": "StructuralComplexity", - "amount": 0.3, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "High cognitive complexity: 16" + "issue": "Code duplication: 17.2%" } ], "critical_defects_count": 0, @@ -41554,77 +40906,6 @@ }, "git_context": null }, - "./src/stdlib/hash.rs": { - "content_hash": [ - 245, - 136, - 56, - 33, - 87, - 85, - 153, - 67, - 145, - 119, - 239, - 236, - 114, - 249, - 67, - 111, - 131, - 234, - 2, - 234, - 140, - 58, - 157, - 139, - 238, - 1, - 24, - 139, - 216, - 107, - 167, - 149 - ], - "score": { - "structural_complexity": 25.0, - "semantic_complexity": 20.0, - "duplication_ratio": 20.0, - "coupling_score": 15.0, - "doc_coverage": 10.0, - "consistency_score": 10.0, - "entropy_score": 7.0, - "total": 97.27273, - "grade": "A+", - "confidence": 1.0, - "language": "Rust", - "file_path": "./src/stdlib/hash.rs", - "penalties_applied": [ - { - "source_metric": "Duplication", - "amount": 3.0, - "applied_to": [ - "Duplication" - ], - "issue": "Found 6 duplicate code patterns" - } - ], - "critical_defects_count": 0, - "has_critical_defects": false, - "has_contract_coverage": false - }, - "components": { - "complexity_breakdown": {}, - "duplication_sources": [], - "coupling_dependencies": [], - "doc_missing_items": [], - "consistency_violations": [] - }, - "git_context": null - }, "./src/stdlib/html.rs": { "content_hash": [ 79, @@ -41951,38 +41232,38 @@ }, "./src/stdlib/mod.rs": { "content_hash": [ - 117, - 213, - 217, - 29, - 113, - 111, - 139, - 37, - 26, - 212, - 6, - 155, - 73, - 91, - 185, - 32, - 197, - 242, - 134, - 168, - 99, - 86, + 55, + 9, + 236, + 68, 184, - 241, - 140, - 194, - 103, - 47, - 69, - 35, - 172, - 190 + 210, + 158, + 74, + 228, + 40, + 201, + 244, + 244, + 73, + 115, + 239, + 105, + 78, + 201, + 222, + 220, + 143, + 120, + 4, + 39, + 144, + 60, + 209, + 122, + 43, + 181, + 140 ], "score": { "structural_complexity": 25.0, @@ -42496,38 +41777,38 @@ }, "./src/stdlib/trueno_bridge.rs": { "content_hash": [ - 180, - 174, - 192, - 124, - 172, - 94, - 141, - 120, - 8, - 132, - 61, - 69, - 97, - 94, - 180, - 86, - 216, - 42, - 200, - 207, - 247, - 224, + 208, + 228, + 201, + 153, + 221, + 3, + 80, + 80, + 80, + 196, + 29, + 242, + 56, 5, - 123, 223, - 15, - 78, - 171, - 176, - 9, - 221, - 44 + 13, + 158, + 239, + 62, + 175, + 108, + 238, + 75, + 34, + 70, + 240, + 146, + 152, + 113, + 70, + 75, + 31 ], "score": { "structural_complexity": 25.0, @@ -44863,14 +44144,14 @@ } }, "summary": { - "total_files": 528, - "avg_score": 90.32976, + "total_files": 519, + "avg_score": 90.31551, "grade_distribution": { - "A+": 195, + "A+": 189, "A": 176, - "A-": 31, - "B+": 29, - "B": 68, + "A-": 30, + "B+": 28, + "B": 67, "B-": 26, "C": 1, "F": 2 @@ -44878,7 +44159,7 @@ "languages": { "JavaScript": 14, "Python": 4, - "Rust": 506, + "Rust": 497, "TypeScript": 4 } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a24171c..f21354184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed — G2 part 2 (RHLGA-1): the next round of workspace test failures + +Behaviour changes a program can observe are marked **(behaviour)**. + +- **(behaviour)** Method receivers follow the spec (§7.11): a bare `self` is + owned, and becomes `&mut self` when the method mutates it. Write `&self` for + a borrowing getter. This reverses the 2026-01 change that always borrowed. +- `module::function()` calls keep `::` in transpiled Rust, and the parser now + records the difference. A closure stored in a field can be called as + `(obj.f)(x)`: field accesses carry their real source span, so the call parses. +- Nested `pub mod` and `pub` items inside a module parse and transpile. +- `@test("…")` transpiles to `#[doc = "…"] #[test]`, and `#[derive(Debug)]` is + accepted, as the 5.0 attribute grammar says. +- A function that returns a `let`-bound `[]` infers `-> Vec<…>`. +- `range(a, b).count()` (and `sum`/`min`/`max`) compile. A `main` that awaits + runs on a small in-program executor, with no tokio needed. +- **(behaviour)** The interpreter's `{:?}` prints what Rust prints (`[1, 2]`, + not `Array([Integer(1), …])`). `println("{:?}", x)` and `format!` substitute + like the macro. `format!` no longer quotes its string arguments. +- **(behaviour)** `compute_hash` returns MD5 (32 hex characters) again, as its + contract says, using an in-tree RFC 1321 implementation. In 5.0.0-beta.2 a + dependency cleanup had silently made it SHA-256. +- **(behaviour)** CLI routing is exhaustive, so no verb can print "not yet + implemented" and exit 0. `ruchy prove` now reaches the prover. + `ruchy infra …` rejects a missing path or a directory. `ruchy serve` returns + an error when it cannot bind its port, and `--watch` keeps serving, with a + warning, when the host has no inotify instance left. +- Tests: + - they spawn the cargo-built binary; + - `ruchy serve` tests stop through a shutdown seam instead of serving forever; + - `release_hygiene` names a dirty tree; + - pmat-written files are gitignored. + ### Added — RHL-4, RHL-5, RHL-6, RHL-9: RHL compiles, tests, contracts and talks MCP (RHL-001, experimental) - **Lowering (RHL-4):** a checked `job` lowers to Ruchy source, following spec diff --git a/src/bin/handlers/command_router.rs b/src/bin/handlers/command_router.rs index ef065197d..de261c8ad 100644 --- a/src/bin/handlers/command_router.rs +++ b/src/bin/handlers/command_router.rs @@ -662,6 +662,7 @@ fn dispatch_tooling(command: crate::Commands) -> Result<()> { | crate::Commands::Fix { .. } | crate::Commands::Convert { .. } | crate::Commands::Vocab { .. } + | crate::Commands::Explain { .. } | crate::Commands::Test { .. } | crate::Commands::PropertyTests { .. } | crate::Commands::Mutations { .. } From 370eb71d724f30cc48a7c8b70d2d469d5de64743 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 02:35:38 +0200 Subject: [PATCH 024/106] COMPILERACE-1: the compile property test wrote a.out into the crate root; router tests exercise the real routes The proptest compiled random strings with the default output, so an empty or whitespace input produced a.out in the working directory. It and two tests writing fixed /tmp paths now compile into their own temp dirs. The six handle_advanced_command tests had passed only through the removed catch-all: parse, transpile, check and compile now go through handle_command_dispatch; publish and repl assert the router refuses a main-routed verb. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/compiler.rs | 17 +++++++++++++---- src/bin/ruchy_tests.rs | 27 ++++++++++++++++++++------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/backend/compiler.rs b/src/backend/compiler.rs index 5912f70fc..b30e6aac3 100644 --- a/src/backend/compiler.rs +++ b/src/backend/compiler.rs @@ -752,8 +752,9 @@ mod tests { println("Hello from compiled Ruchy!"); } "#; + let dir = tempfile::TempDir::new().expect("temp dir"); let options = CompileOptions { - output: PathBuf::from("/tmp/test_ruchy_binary"), + output: dir.path().join("out"), ..Default::default() }; // This might fail if the transpiler doesn't support the syntax yet @@ -1185,8 +1186,9 @@ mod tests { ]; for source in complex_sources { + let dir = tempfile::TempDir::new().expect("temp dir"); let options = CompileOptions { - output: PathBuf::from("/tmp/complex_test"), + output: dir.path().join("out"), ..Default::default() }; @@ -1391,10 +1393,17 @@ mod property_tests_compiler { #[test] fn test_compile_source_to_binary_never_panics(input: String) { // Limit input size to avoid timeout - let _input = if input.len() > 100 { &input[..100] } else { &input[..] }; + let input: String = input.chars().take(100).collect(); + // COMPILERACE-1: a random input can be a valid program (for example + // an empty one), so the binary goes into a temp dir, never a.out in + // the crate root. + let dir = tempfile::TempDir::new().expect("operation should succeed in test"); + let options = CompileOptions { + output: dir.path().join("out"), + ..Default::default() + }; // Function should not panic on any input, even invalid syntax let result = std::panic::catch_unwind(|| { - let options = CompileOptions::default(); let _ = compile_source_to_binary(&input, &options); }); // Assert that no panic occurred (Result can be Ok or Err, but no panic) diff --git a/src/bin/ruchy_tests.rs b/src/bin/ruchy_tests.rs index cf0fc874f..d9d0b525c 100644 --- a/src/bin/ruchy_tests.rs +++ b/src/bin/ruchy_tests.rs @@ -205,8 +205,12 @@ fn test_handle_advanced_command_repl() { record: None, max_depth: 100, }; + // Main routes this verb before the router; the router must refuse it + // rather than report a silent success (G2B-S2). Running it here would + // publish or block on stdin; its handler has its own tests. let result = handle_advanced_command(command); - assert!(result.is_ok()); + let err = result.expect_err("the router must not accept a main-routed verb"); + assert!(err.to_string().contains("routing"), "{err}"); } #[test] @@ -217,7 +221,8 @@ fn test_handle_advanced_command_parse() { let command = Commands::Parse { file: temp_file.path().to_path_buf(), }; - let result = handle_advanced_command(command); + // Main routes this verb itself; exercise that real route. + let result = handle_command_dispatch(Some(command), false, VmMode::Ast); assert!(result.is_ok()); } @@ -232,7 +237,8 @@ fn test_handle_advanced_command_transpile() { minimal: false, emit: None, }; - let result = handle_advanced_command(command); + // Main routes this verb itself; exercise that real route. + let result = handle_command_dispatch(Some(command), false, VmMode::Ast); assert!(result.is_ok()); } @@ -241,9 +247,10 @@ fn test_handle_advanced_command_compile() { let temp_file = NamedTempFile::new().expect("Failed to create temporary test file"); fs::write(&temp_file, "let x = 42").expect("Failed to write test content to temporary file"); + let out_dir = tempfile::TempDir::new().expect("temp dir"); let command = Commands::Compile { file: temp_file.path().to_path_buf(), - output: PathBuf::from("test.out"), + output: out_dir.path().join("test.out"), opt_level: "2".to_string(), optimize: None, strip: false, @@ -255,7 +262,8 @@ fn test_handle_advanced_command_compile() { pgo: false, embed_models: Vec::new(), }; - let result = handle_advanced_command(command); + // Main routes this verb itself; exercise that real route. + let result = handle_command_dispatch(Some(command), false, VmMode::Ast); assert!(result.is_ok()); } @@ -269,7 +277,8 @@ fn test_handle_advanced_command_check() { watch: false, format: "text".to_string(), }; - let result = handle_advanced_command(command); + // Main routes this verb itself; exercise that real route. + let result = handle_command_dispatch(Some(command), false, VmMode::Ast); assert!(result.is_ok()); } @@ -423,8 +432,12 @@ fn test_handle_advanced_command_publish() { dry_run: true, allow_dirty: false, }; + // Main routes this verb before the router; the router must refuse it + // rather than report a silent success (G2B-S2). Running it here would + // publish or block on stdin; its handler has its own tests. let result = handle_advanced_command(command); - assert!(result.is_ok()); + let err = result.expect_err("the router must not accept a main-routed verb"); + assert!(err.to_string().contains("routing"), "{err}"); } #[test] From 8ff0de2a1c58b82830cad7c44f97880a0e37ebfa Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 02:52:44 +0200 Subject: [PATCH 025/106] G2BFB1: serve --watch returns the server's error when it cannot bind; a restart awaits the aborted server - run_watch_mode polls the server task like normal mode (poll_server) and returns its outcome when it ends on its own; a file change aborts the task and awaits it before rebinding (serve_until_change, stopped_on_change, spawn_server) - test_serve_until_watch_mode_reports_a_port_in_use (RED: hung past 20 s) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/bin/handlers/serve_handler.rs | 141 +++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 34 deletions(-) diff --git a/src/bin/handlers/serve_handler.rs b/src/bin/handlers/serve_handler.rs index 3744b17f1..6bdd23b3e 100644 --- a/src/bin/handlers/serve_handler.rs +++ b/src/bin/handlers/serve_handler.rs @@ -258,8 +258,11 @@ fn print_startup_banner(host: &str, port: u16, directory: &Path, watch: bool, wa } } -/// Run server in watch mode with file change detection -/// Complexity: 8 (Toyota Way: <10) +/// Run server in watch mode with file change detection. +/// +/// A server that ends on its own (e.g. the address cannot be bound) returns +/// its error, as in normal mode. +/// Complexity: 3 (Toyota Way: <10) #[cfg(feature = "notebook")] #[allow(clippy::too_many_arguments)] fn run_watch_mode( @@ -288,37 +291,61 @@ fn run_watch_mode( ); }; loop { - let addr = format!("{}:{}", host, port); - let app_clone = app.clone(); - let server_handle = runtime.spawn(async move { - let listener = tokio::net::TcpListener::bind(&addr).await?; - - if verbose { - println!("✅ Server started ({} workers)", num_cpus); - } - - axum::serve(listener, app_clone).await - }); - - // Poll for file changes AND shutdown signal - loop { - // Check for shutdown signal - if shutdown_rx.try_recv().is_ok() { - print_shutdown_message(); - server_handle.abort(); - return Ok(()); - } - - if let Some(changed_files) = watcher.check_changes() { - handle_file_changes(&changed_files, watch_wasm, verbose); - server_handle.abort(); - print_restart_message(); - break; - } + let server_handle = spawn_server(runtime, app.clone(), host, port, verbose, num_cpus); + if let Some(outcome) = serve_until_change( + runtime, + server_handle, + &mut watcher, + watch_wasm, + verbose, + shutdown_rx, + ) { + return outcome; + } + } +} - std::thread::sleep(std::time::Duration::from_millis(100)); +/// Serve until shutdown is requested or the server task ends (`Some`, the +/// outcome) or a watched file changes (`None`, the server has stopped and its +/// address is free to bind again). +/// Complexity: 3 (Toyota Way: <10) +#[cfg(feature = "notebook")] +fn serve_until_change( + runtime: &tokio::runtime::Runtime, + mut server_handle: tokio::task::JoinHandle>, + watcher: &mut ruchy::server::watcher::FileWatcher, + watch_wasm: bool, + verbose: bool, + shutdown_rx: &std::sync::mpsc::Receiver<()>, +) -> Option> { + while !stopped_on_change(runtime, &mut server_handle, watcher, watch_wasm, verbose) { + if let Some(outcome) = poll_server(runtime, &mut server_handle, shutdown_rx) { + return Some(outcome); } + std::thread::sleep(std::time::Duration::from_millis(100)); } + None +} + +/// When a watched file changed: handle it, stop the server and wait for the +/// aborted task so its listener is closed before the restart rebinds (`true`). +/// Complexity: 2 (Toyota Way: <10) +#[cfg(feature = "notebook")] +fn stopped_on_change( + runtime: &tokio::runtime::Runtime, + server_handle: &mut tokio::task::JoinHandle>, + watcher: &mut ruchy::server::watcher::FileWatcher, + watch_wasm: bool, + verbose: bool, +) -> bool { + let Some(changed_files) = watcher.check_changes() else { + return false; + }; + handle_file_changes(&changed_files, watch_wasm, verbose); + server_handle.abort(); + let _ = runtime.block_on(server_handle); + print_restart_message(); + true } /// Open the file watcher for `directory`, or `None` (with a warning on stderr) @@ -439,8 +466,23 @@ fn run_normal_mode( num_cpus: usize, shutdown_rx: &std::sync::mpsc::Receiver<()>, ) -> Result<()> { + let server_handle = spawn_server(runtime, app, host, port, verbose, num_cpus); + wait_for_shutdown_or_exit(runtime, server_handle, shutdown_rx) +} + +/// Spawn the server task: bind `host:port`, then serve `app` on it. +/// Complexity: 2 (Toyota Way: <10) +#[cfg(feature = "notebook")] +fn spawn_server( + runtime: &tokio::runtime::Runtime, + app: axum::Router, + host: &str, + port: u16, + verbose: bool, + num_cpus: usize, +) -> tokio::task::JoinHandle> { let addr = format!("{}:{}", host, port); - let server_handle = runtime.spawn(async move { + runtime.spawn(async move { let listener = tokio::net::TcpListener::bind(&addr).await?; if verbose { @@ -448,9 +490,7 @@ fn run_normal_mode( } axum::serve(listener, app).await - }); - - wait_for_shutdown_or_exit(runtime, server_handle, shutdown_rx) + }) } /// Block until shutdown is requested (abort the server, `Ok`) or the server @@ -693,6 +733,39 @@ mod tests { serve_until(&request, stopped).expect("watch mode stops"); } + /// RHLGA-1: watch mode on an address it cannot bind returns the bind error + /// promptly instead of watching files with no server behind them. + #[test] + #[cfg(feature = "notebook")] + fn test_serve_until_watch_mode_reports_a_port_in_use() { + let temp_dir = TempDir::new().unwrap(); + let taken = std::net::TcpListener::bind("127.0.0.1:0").expect("bind port 0"); + let port = taken.local_addr().expect("local addr").port(); + let dir = temp_dir.path().to_path_buf(); + let (done_tx, done_rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let (_tx, rx) = std::sync::mpsc::channel::<()>(); + let request = ServeRequest { + directory: &dir, + port, + host: "127.0.0.1", + verbose: false, + watch: true, + debounce: 50, + pid_file: None, + watch_wasm: false, + }; + let outcome = serve_until(&request, move || rx).map_err(|e| e.to_string()); + let _ = done_tx.send(outcome); + }); + let outcome = done_rx + .recv_timeout(std::time::Duration::from_secs(20)) + .expect("watch mode must return when the server cannot bind"); + let err = outcome.expect_err("port is taken"); + assert!(err.to_lowercase().contains("in use"), "{err}"); + drop(taken); + } + // ===== EXTREME TDD Round 145 - Serve Handler Tests ===== #[test] From b4d781922d135d31c81f08d952b74e78b36414d7 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 02:57:05 +0200 Subject: [PATCH 026/106] G2BFA1: trait default methods with a by-value self get where Self: Sized; G2BFA2: &mut self sees nested/indexed field writes, field ++/--, mutating std methods on fields and mutating sibling calls (fixpoint) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/mutation_detection.rs | 223 ++++++----- src/backend/transpiler/types.rs | 198 ++++++---- tests/g2b_fix_a.rs | 379 +++++++++++++++++++ 3 files changed, 649 insertions(+), 151 deletions(-) create mode 100644 tests/g2b_fix_a.rs diff --git a/src/backend/transpiler/mutation_detection.rs b/src/backend/transpiler/mutation_detection.rs index ab32948cd..7117d701e 100644 --- a/src/backend/transpiler/mutation_detection.rs +++ b/src/backend/transpiler/mutation_detection.rs @@ -1,16 +1,28 @@ //! Mutation detection for transpiler //! //! This module provides functions to detect if variables are mutated -//! (reassigned or modified) within expression trees. +//! (reassigned or modified) within expression trees, and whether a method +//! body mutates `self` (for `&mut self` receiver inference). use crate::frontend::ast::{Expr, ExprKind}; +use std::collections::HashSet; + +/// Std methods that take `&mut self`. A call of one of these on a field of +/// `self` (`self.items.push(x)`, `self.buf.push_str(s)`, `self.map.entry(k)`) +/// mutates `self`. The list is deliberately closed: a method not listed here +/// is treated as non-mutating unless it is a mutating method of the same impl. +pub const MUTATING_STD_METHODS: &[&str] = &[ + "push", "pop", "insert", "remove", "clear", "extend", "append", "truncate", "sort", "sort_by", + "reverse", "dedup", "retain", "drain", "push_str", "swap", "entry", +]; /// Checks if a variable is mutated (reassigned or modified) in an expression tree /// -/// This function traverses the AST recursively to detect: -/// - Direct assignments (`x = value`) -/// - Compound assignments (`x += 1`, `x -= 1`, etc.) -/// - Increment/decrement operations (`x++`, `++x`, `x--`, `--x`) +/// This function traverses the AST recursively to detect writes through a +/// place rooted at the variable: +/// - Direct assignments (`x = value`, `x.a.b = value`, `x.v[i] = value`) +/// - Compound assignments (`x += 1`, `x.f -= 1`, etc.) +/// - Increment/decrement operations (`x++`, `++x`, `x.f--`, `--x.f`) /// /// # Examples /// ```ignore @@ -19,106 +31,143 @@ use crate::frontend::ast::{Expr, ExprKind}; /// assert!(is_variable_mutated("x", &expr)); /// ``` pub fn is_variable_mutated(name: &str, expr: &Expr) -> bool { - match &expr.kind { - // Direct assignment to the variable - ExprKind::Assign { target, value: _ } => { - if let ExprKind::Identifier(var_name) = &target.kind { - if var_name == name { - return true; - } - } - // TRANSPILER-METHOD-SELF-001 FIX: Detect field mutation (self.field = value) - if let ExprKind::FieldAccess { object, .. } = &target.kind { - if let ExprKind::Identifier(var_name) = &object.kind { - if var_name == name { - return true; - } - } - } - false + any_expr(expr, &|e| writes_place_rooted_at(name, e)) +} + +/// True when a method body mutates `self`: a write through a place rooted at +/// `self`, a [`MUTATING_STD_METHODS`] call on a field of `self`, or a call +/// `self.m(..)` where `m` is in `mutating_methods` (methods of the same impl +/// already known to mutate `self`). +pub fn is_self_mutated(body: &Expr, mutating_methods: &HashSet) -> bool { + any_expr(body, &|e| { + writes_place_rooted_at("self", e) || is_self_mutating_call(e, mutating_methods) + }) +} + +/// Fixpoint over the methods of one impl/trait: the set of method names that +/// mutate `self`, directly or by calling another mutating method of the set. +/// `seed` holds methods known to mutate up front (declared `&mut self`); +/// `candidates` are the `(name, body)` pairs whose receiver is inferred. +pub fn mutating_self_methods( + candidates: &[(&str, &Expr)], + seed: HashSet, +) -> HashSet { + let mut mutating = seed; + loop { + let newly: Vec = candidates + .iter() + .filter(|(name, body)| !mutating.contains(*name) && is_self_mutated(body, &mutating)) + .map(|(name, _)| (*name).to_string()) + .collect(); + if newly.is_empty() { + return mutating; } - // Compound assignment (+=, -=, etc.) - ExprKind::CompoundAssign { - target, value: _, .. - } => { - if let ExprKind::Identifier(var_name) = &target.kind { - if var_name == name { - return true; - } - } - // TRANSPILER-METHOD-SELF-001 FIX: Detect field compound assignment (self.field += value) - if let ExprKind::FieldAccess { object, .. } = &target.kind { - if let ExprKind::Identifier(var_name) = &object.kind { - if var_name == name { - return true; - } - } - } - false + mutating.extend(newly); + } +} + +/// `self.(..)` with `m` a mutating sibling, or `self..(..)` with +/// `m` a mutating std method. +fn is_self_mutating_call(expr: &Expr, mutating_methods: &HashSet) -> bool { + let ExprKind::MethodCall { + receiver, method, .. + } = &expr.kind + else { + return false; + }; + match &receiver.kind { + ExprKind::Identifier(root) => root == "self" && mutating_methods.contains(method), + _ => { + place_root(receiver) == Some("self") && MUTATING_STD_METHODS.contains(&method.as_str()) } - // Pre/Post increment/decrement - ExprKind::PreIncrement { target } + } +} + +/// Assignment, compound assignment or `++`/`--` whose target place is rooted at `name`. +fn writes_place_rooted_at(name: &str, expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Assign { target, .. } + | ExprKind::CompoundAssign { target, .. } + | ExprKind::PreIncrement { target } | ExprKind::PostIncrement { target } | ExprKind::PreDecrement { target } - | ExprKind::PostDecrement { target } => { - if let ExprKind::Identifier(var_name) = &target.kind { - if var_name == name { - return true; - } - } - false + | ExprKind::PostDecrement { target } => place_root(target) == Some(name), + _ => false, + } +} + +/// Root variable of a place expression: `x`, `x.a.b`, `x[i]`, `x.v[i].c` all give `x`. +fn place_root(expr: &Expr) -> Option<&str> { + match &expr.kind { + ExprKind::Identifier(name) => Some(name.as_str()), + ExprKind::FieldAccess { object, .. } | ExprKind::IndexAccess { object, .. } => { + place_root(object) } - // Check in blocks - ExprKind::Block(exprs) => exprs.iter().any(|e| is_variable_mutated(name, e)), - // Check in if branches + _ => None, + } +} + +/// True when `pred` holds for `expr` or any expression nested in it. +fn any_expr(expr: &Expr, pred: &dyn Fn(&Expr) -> bool) -> bool { + pred(expr) || sub_expressions(expr).into_iter().any(|e| any_expr(e, pred)) +} + +/// The direct sub-expressions searched for mutations. +fn sub_expressions(expr: &Expr) -> Vec<&Expr> { + match &expr.kind { + ExprKind::Block(exprs) | ExprKind::List(exprs) | ExprKind::Tuple(exprs) => { + exprs.iter().collect() + } + ExprKind::Macro { args, .. } => args.iter().collect(), ExprKind::If { condition, then_branch, else_branch, - } => { - is_variable_mutated(name, condition) - || is_variable_mutated(name, then_branch) - || else_branch - .as_ref() - .is_some_and(|e| is_variable_mutated(name, e)) - } - // Check in while loops + } => with_optional(vec![condition, then_branch], else_branch.as_deref()), + ExprKind::IfLet { + expr, + then_branch, + else_branch, + .. + } => with_optional(vec![expr, then_branch], else_branch.as_deref()), ExprKind::While { condition, body, .. - } => is_variable_mutated(name, condition) || is_variable_mutated(name, body), - // Check in for loops - ExprKind::For { body, .. } => is_variable_mutated(name, body), - // Check in match expressions - ExprKind::Match { expr, arms } => { - is_variable_mutated(name, expr) - || arms.iter().any(|arm| is_variable_mutated(name, &arm.body)) - } - // Check in nested let expressions - ExprKind::Let { body, .. } | ExprKind::LetPattern { body, .. } => { - is_variable_mutated(name, body) - } - // Check in function bodies - ExprKind::Function { body, .. } => is_variable_mutated(name, body), - // Check in lambda bodies - ExprKind::Lambda { body, .. } => is_variable_mutated(name, body), - // Check binary operations - ExprKind::Binary { left, right, .. } => { - is_variable_mutated(name, left) || is_variable_mutated(name, right) + } => vec![&**condition, &**body], + ExprKind::WhileLet { expr, body, .. } + | ExprKind::For { + iter: expr, body, .. + } => { + vec![&**expr, &**body] } - // Check unary operations - ExprKind::Unary { operand, .. } => is_variable_mutated(name, operand), - // Check function/method calls - ExprKind::Call { func, args } => { - is_variable_mutated(name, func) || args.iter().any(|a| is_variable_mutated(name, a)) + ExprKind::Match { expr, arms } => std::iter::once(&**expr) + .chain(arms.iter().map(|arm| &*arm.body)) + .collect(), + ExprKind::Let { value, body, .. } | ExprKind::LetPattern { value, body, .. } => { + vec![&**value, &**body] } + ExprKind::Binary { left, right, .. } => vec![&**left, &**right], + ExprKind::Loop { body, .. } + | ExprKind::Function { body, .. } + | ExprKind::Lambda { body, .. } + | ExprKind::Unary { operand: body, .. } + | ExprKind::Try { expr: body } + | ExprKind::Await { expr: body } + | ExprKind::Assign { value: body, .. } + | ExprKind::CompoundAssign { value: body, .. } => vec![&**body], + ExprKind::Return { value } => with_optional(Vec::new(), value.as_deref()), + ExprKind::Call { func, args } => std::iter::once(&**func).chain(args).collect(), ExprKind::MethodCall { receiver, args, .. } => { - is_variable_mutated(name, receiver) || args.iter().any(|a| is_variable_mutated(name, a)) + std::iter::once(&**receiver).chain(args).collect() } - // Other expressions don't contain mutations - _ => false, + _ => Vec::new(), } } +/// `required` (as plain references) followed by `optional` when present. +fn with_optional<'a>(required: Vec<&'a Box>, optional: Option<&'a Expr>) -> Vec<&'a Expr> { + required.into_iter().map(|e| &**e).chain(optional).collect() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/backend/transpiler/types.rs b/src/backend/transpiler/types.rs index 312316af1..4f3f7135a 100644 --- a/src/backend/transpiler/types.rs +++ b/src/backend/transpiler/types.rs @@ -10,6 +10,7 @@ use crate::frontend::ast::{ use anyhow::{bail, Result}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use std::collections::HashSet; use syn::Lifetime; /// Generate self-receiver tokens for a `self` parameter. @@ -41,6 +42,70 @@ fn self_receiver_tokens_owned(ty: &TypeKind) -> TokenStream { } } +/// The `self` parameter of a method, when its first parameter is `self`. +fn self_param(params: &[crate::frontend::ast::Param]) -> Option<&crate::frontend::ast::Param> { + params.first().filter(|p| p.name() == "self") +} + +/// Declared `&mut self`: mutates `self` whatever its body does. +fn declares_mut_self(params: &[crate::frontend::ast::Param]) -> bool { + self_param(params) + .is_some_and(|p| matches!(p.ty.kind, TypeKind::Reference { is_mut: true, .. })) +} + +/// Bare `self` (neither a reference nor `mut self`): the receiver is inferred from the body. +fn has_inferred_self(params: &[crate::frontend::ast::Param]) -> bool { + self_param(params) + .is_some_and(|p| !p.is_mutable && !matches!(p.ty.kind, TypeKind::Reference { .. })) +} + +/// The receiver is emitted by value (`self` / `mut self`), see [`self_receiver_tokens`]. +fn has_by_value_receiver(params: &[crate::frontend::ast::Param], mutated: bool) -> bool { + self_param(params).is_some_and(|p| { + !matches!(p.ty.kind, TypeKind::Reference { .. }) && (p.is_mutable || !mutated) + }) +} + +/// Methods of one impl that mutate `self` (fixpoint over sibling calls). +fn impl_mutating_methods(methods: &[ImplMethod]) -> HashSet { + let seed = methods + .iter() + .filter(|m| declares_mut_self(&m.params)) + .map(|m| m.name.clone()) + .collect(); + let candidates: Vec<(&str, &crate::frontend::ast::Expr)> = methods + .iter() + .filter(|m| has_inferred_self(&m.params)) + .map(|m| (m.name.as_str(), &*m.body)) + .collect(); + mutation_detection::mutating_self_methods(&candidates, seed) +} + +/// Whether an impl method's `self` is mutated: from the impl fixpoint for a bare +/// `self`, from the body for an explicit receiver. +fn impl_self_is_mutated(method: &ImplMethod, mutating: &HashSet) -> bool { + if has_inferred_self(&method.params) { + mutating.contains(&method.name) + } else { + mutation_detection::is_variable_mutated("self", &method.body) + } +} + +/// Methods of one trait that mutate `self`; only default bodies can be inspected. +fn trait_mutating_methods(methods: &[TraitMethod]) -> HashSet { + let seed = methods + .iter() + .filter(|m| declares_mut_self(&m.params)) + .map(|m| m.name.clone()) + .collect(); + let candidates: Vec<(&str, &crate::frontend::ast::Expr)> = methods + .iter() + .filter(|m| has_inferred_self(&m.params)) + .filter_map(|m| m.body.as_deref().map(|body| (m.name.as_str(), body))) + .collect(); + mutation_detection::mutating_self_methods(&candidates, seed) +} + impl Transpiler { /// Transpiles type annotations /// @@ -1063,66 +1128,11 @@ impl Transpiler { }) .collect(); - let method_tokens: Result> = methods + let mutating = trait_mutating_methods(methods); + let method_tokens = methods .iter() - .map(|method| { - let method_name = format_ident!("{}", method.name); - // TRANSPILER-TRAIT-001 FIX: Determine if self is mutated for &mut self inference - // For traits with default implementations, check the body for mutations - let self_is_mutated = method.body.as_ref().is_some_and(|body| { - crate::backend::transpiler::mutation_detection::is_variable_mutated( - "self", body, - ) - }); - // Process parameters - let param_tokens: Vec = method - .params - .iter() - .enumerate() - .map(|(i, param)| { - if i == 0 && (param.name() == "self" || param.name() == "&self") { - // TRANSPILER-TRAIT-001 FIX: Handle self parameter consistently - if param.name().starts_with('&') { - // Legacy: name-based detection (e.g., "&self" as name) - quote! { &self } - } else { - self_receiver_tokens(param, self_is_mutated) - } - } else { - let param_name = format_ident!("{}", param.name()); - let type_tokens = self - .transpile_type(¶m.ty) - .unwrap_or_else(|_| quote! { _ }); - quote! { #param_name: #type_tokens } - } - }) - .collect(); - // Process return type - let return_type_tokens = if let Some(ref ty) = method.return_type { - let ty_tokens = self.transpile_type(ty)?; - quote! { -> #ty_tokens } - } else { - quote! {} - }; - // TRANSPILER-TRAIT-001 FIX: Trait methods cannot have visibility modifiers - // In Rust, trait method declarations are implicitly public. - // Adding `pub` causes prettyplease to fail with "not implemented: TraitItem::Verbatim" - // Process method body (if default implementation) - if let Some(ref body) = method.body { - let body_tokens = self.transpile_expr(body)?; - Ok(quote! { - fn #method_name(#(#param_tokens),*) #return_type_tokens { - #body_tokens - } - }) - } else { - Ok(quote! { - fn #method_name(#(#param_tokens),*) #return_type_tokens; - }) - } - }) - .collect(); - let method_tokens = method_tokens?; + .map(|method| self.transpile_trait_method(method, &mutating)) + .collect::>>()?; let type_param_tokens: Vec<_> = type_params .iter() .map(|p| Self::parse_type_param_to_tokens(p)) @@ -1148,6 +1158,67 @@ impl Transpiler { }) } } + /// One trait method: a declaration, or a default method when it has a body. + /// A default method whose receiver is by value gets `where Self: Sized` (the + /// body moves `self`, rustc E0277 otherwise); a bodiless one needs no bound. + fn transpile_trait_method( + &self, + method: &TraitMethod, + mutating: &HashSet, + ) -> Result { + let method_name = format_ident!("{}", method.name); + // TRANSPILER-TRAIT-001: bare `self` in a default method that mutates it is `&mut self` + let self_is_mutated = mutating.contains(&method.name); + let param_tokens: Vec = method + .params + .iter() + .enumerate() + .map(|(i, param)| self.trait_param_tokens(i, param, self_is_mutated)) + .collect(); + let return_type_tokens = match method.return_type { + Some(ref ty) => { + let ty_tokens = self.transpile_type(ty)?; + quote! { -> #ty_tokens } + } + None => quote! {}, + }; + // TRANSPILER-TRAIT-001: trait methods carry no visibility (implicitly public) + let Some(ref body) = method.body else { + return Ok(quote! { fn #method_name(#(#param_tokens),*) #return_type_tokens; }); + }; + let body_tokens = self.transpile_expr(body)?; + let sized_bound = if has_by_value_receiver(&method.params, self_is_mutated) { + quote! { where Self: Sized } + } else { + quote! {} + }; + Ok(quote! { + fn #method_name(#(#param_tokens),*) #return_type_tokens #sized_bound { + #body_tokens + } + }) + } + + /// Parameter `i` of a trait method; the first one may be the `self` receiver. + fn trait_param_tokens( + &self, + i: usize, + param: &crate::frontend::ast::Param, + self_is_mutated: bool, + ) -> TokenStream { + match param.name().as_str() { + // Legacy: name-based detection ("&self" as the parameter name) + "&self" if i == 0 => quote! { &self }, + "self" if i == 0 => self_receiver_tokens(param, self_is_mutated), + _ => { + let param_name = format_ident!("{}", param.name()); + let type_tokens = self + .transpile_type(¶m.ty) + .unwrap_or_else(|_| quote! { _ }); + quote! { #param_name: #type_tokens } + } + } + } /// Transpiles impl blocks pub fn transpile_impl( &self, @@ -1161,17 +1232,16 @@ impl Transpiler { // e.g., "Container" -> "Container" let base_type = for_type.split('<').next().unwrap_or(for_type).trim(); let type_ident = format_ident!("{}", base_type); + let mutating = impl_mutating_methods(methods); let method_tokens: Result> = methods .iter() .map(|method| { let method_name = format_ident!("{}", method.name); // TRANSPILER-METHOD-SELF-001 FIX: Check if self is mutated in method body // to infer &mut self vs &self when not explicitly annotated - let self_is_mutated = - crate::backend::transpiler::mutation_detection::is_variable_mutated( - "self", - &method.body, - ); + // RHLGA-1: bare `self` also mutates through nested/indexed fields, mutating + // std methods on a field and calls to mutating sibling methods (fixpoint) + let self_is_mutated = impl_self_is_mutated(method, &mutating); // Process parameters let param_tokens: Vec = method .params diff --git a/tests/g2b_fix_a.rs b/tests/g2b_fix_a.rs new file mode 100644 index 000000000..1b3841aaa --- /dev/null +++ b/tests/g2b_fix_a.rs @@ -0,0 +1,379 @@ +//! G2B-FIX-A (RHLGA-1): review findings on the G2 part 2 branch. +//! +//! 1. A trait default method with a by-value `self` needs `where Self: Sized`. +//! 2. `&mut self` inference sees nested/indexed field writes, field `++`/`--`, +//! mutating std methods on a field and calls to mutating sibling methods. +//! 3. The `::` path marker survives `ruchy fmt` and non-callee transpilation. +//! 4. A user-defined `range` function is not the builtin range. +//! 5. An awaiting `main` keeps a declared non-unit return type. +//! +//! Every program is transpiled, compiled with rustc and run. + +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] + +use ruchy::quality::formatter::Formatter; +use ruchy::{Parser, Transpiler}; +use std::process::Command; + +fn scratch_dir(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("g2b_fix_a_{}_{name}", std::process::id())); + std::fs::create_dir_all(&dir).expect("scratch dir"); + dir +} + +fn transpile(code: &str) -> String { + let ast = Parser::new(code).parse().expect("parses"); + Transpiler::new() + .transpile_to_program(&ast) + .expect("transpiles") + .to_string() +} + +fn compiled_output(code: &str, name: &str) -> String { + let rust = transpile(code); + let dir = scratch_dir(name); + let src = dir.join("main.rs"); + let bin = dir.join("main"); + std::fs::write(&src, &rust).expect("write rust"); + let out = Command::new("rustc") + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&src) + .output() + .expect("rustc runs"); + assert!( + out.status.success(), + "rustc rejected the transpiled program:\n{}\n--- rust ---\n{rust}", + String::from_utf8_lossy(&out.stderr) + ); + let run = Command::new(&bin).output().expect("binary runs"); + assert!(run.status.success(), "binary failed: {run:?}"); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +/// Whitespace-free token text, so `where Self : Sized` matches `whereSelf:Sized`. +fn squash(rust: &str) -> String { + rust.chars().filter(|c| !c.is_whitespace()).collect() +} + +// ------------------------------------------- 1. trait default by-value self + +const TRAIT_DEFAULT_OWNED: &str = r" +trait Named { + fun id(self) -> i32 + fun consume(self) -> i32 { + self.id() + 1 + } +} +struct Token { + v: i32 +} +impl Named for Token { + fun id(self) -> i32 { + self.v + } +} +fun main() { + let t = Token { v: 41 } + println(t.consume()) +} +"; + +#[test] +fn test_g2bfa1_trait_default_owned_self_compiles() { + assert_eq!(compiled_output(TRAIT_DEFAULT_OWNED, "fa1_default"), "42\n"); +} + +#[test] +fn test_g2bfa1_trait_default_owned_self_gets_sized_bound() { + let rust = squash(&transpile(TRAIT_DEFAULT_OWNED)); + assert!( + rust.contains("fnconsume(self)->i32whereSelf:Sized{"), + "default by-value method needs `where Self: Sized`: {rust}" + ); + assert!( + rust.contains("fnid(self)->i32;"), + "a bodiless by-value declaration needs no bound: {rust}" + ); +} + +#[test] +fn test_g2bfa1_spec_7_1_owned_self_default_compiles() { + let code = r#" +trait Actor { + fun launch(self) -> i32 { + self.id() * 2 + } + fun id(&self) -> i32 +} +struct Worker { + n: i32 +} +impl Actor for Worker { + fun id(&self) -> i32 { + self.n + } +} +fun main() { + let w = Worker { n: 21 } + println(w.launch()) +} +"#; + assert_eq!(compiled_output(code, "fa1_spawn"), "42\n"); +} + +#[test] +fn test_g2bfa1_trait_default_borrowed_self_has_no_bound() { + let code = r" +trait Shown { + fun show(&self) -> i32 { + 7 + } +} +fun main() { +} +"; + let rust = squash(&transpile(code)); + assert!(rust.contains("fnshow(&self)->i32{"), "{rust}"); + assert!(!rust.contains("Sized"), "{rust}"); +} + +// ---------------------------------------------- 2. &mut self inference + +fn assert_mut_self(code: &str, method: &str) { + let rust = squash(&transpile(code)); + assert!( + rust.contains(&format!("fn{method}(&mutself")), + "`{method}` mutates self and must take &mut self: {rust}" + ); +} + +#[test] +fn test_g2bfa2_nested_field_assignment_is_mut_self() { + let code = r" +struct Inner { + n: i32 +} +struct Outer { + inner: Inner +} +impl Outer { + fun bump(self) { + self.inner.n = self.inner.n + 1 + } +} +fun main() { + let mut o = Outer { inner: Inner { n: 1 } } + o.bump() + println(o.inner.n) +} +"; + assert_mut_self(code, "bump"); + assert_eq!(compiled_output(code, "fa2_nested"), "2\n"); +} + +#[test] +fn test_g2bfa2_indexed_field_assignment_is_mut_self() { + let code = r" +struct Grid { + cells: Vec +} +impl Grid { + fun set(self, i: usize, v: i32) { + self.cells[i] = v + } +} +fun main() { + let mut g = Grid { cells: vec![0, 0, 0] } + g.set(1, 5) + println(g.cells[1]) +} +"; + assert_mut_self(code, "set"); + assert_eq!(compiled_output(code, "fa2_index"), "5\n"); +} + +#[test] +fn test_g2bfa2_field_increment_is_mut_self() { + let code = r" +struct Counter { + count: i32 +} +impl Counter { + fun tick(self) -> i32 { + self.count++ + } + fun untick(self) -> i32 { + --self.count + } +} +fun main() { + let mut c = Counter { count: 0 } + c.tick() + c.tick() + c.untick() + println(c.count) +} +"; + assert_mut_self(code, "tick"); + assert_mut_self(code, "untick"); + assert_eq!(compiled_output(code, "fa2_incr"), "1\n"); +} + +#[test] +fn test_g2bfa2_mutating_std_method_on_field_is_mut_self() { + let code = r" +struct Bag { + items: Vec +} +impl Bag { + fun add(self, x: i32) { + self.items.push(x) + } + fun size(self) -> usize { + self.items.len() + } +} +fun main() { + let mut b = Bag { items: vec![] } + b.add(3) + b.add(4) + println(b.items.len()) +} +"; + assert_mut_self(code, "add"); + let rust = squash(&transpile(code)); + assert!( + rust.contains("fnsize(self)->usize"), + "a read-only method stays an owned receiver: {rust}" + ); + assert_eq!(compiled_output(code, "fa2_push"), "2\n"); +} + +#[test] +fn test_g2bfa2_calling_mutating_sibling_is_mut_self() { + // examples/self_hosting/lexer.ruchy: skip_whitespace(self) calls self.advance() + let code = r" +struct Lexer { + pos: i32 +} +impl Lexer { + fun skip_whitespace(self) { + while self.pos < 3 { + self.advance() + } + } + fun skip_twice(self) { + self.skip_whitespace() + } + fun advance(self) { + self.pos = self.pos + 1 + } +} +fun main() { + let mut l = Lexer { pos: 0 } + l.skip_twice() + println(l.pos) +} +"; + assert_mut_self(code, "advance"); + assert_mut_self(code, "skip_whitespace"); + assert_mut_self(code, "skip_twice"); + assert_eq!(compiled_output(code, "fa2_sibling"), "3\n"); +} + +// ------------------------------------------------- 3. `::` path marker + +fn fmt(code: &str) -> String { + let ast = Parser::new(code).parse().expect("parses"); + let mut formatter = Formatter::new(); + formatter.set_source(code); + formatter.format(&ast).expect("formats") +} + +#[test] +fn test_g2bfa3_fmt_keeps_path_separator() { + let out = fmt("helpers::double(4)\n"); + assert!(out.contains("helpers::double(4)"), "fmt output: {out}"); +} + +#[test] +fn test_g2bfa3_fmt_keeps_non_callee_path_and_dot_field() { + let out = fmt("let m = i32::MAX\nlet f = p.x\n"); + assert!(out.contains("i32::MAX"), "fmt output: {out}"); + assert!(out.contains("p.x"), "fmt output: {out}"); + assert!(!out.contains("p::x"), "fmt output: {out}"); +} + +#[test] +fn test_g2bfa3_fmt_round_trip_is_stable() { + let once = fmt("fun main() {\n let v = helpers::double(4)\n}\n"); + let twice = fmt(&once); + assert_eq!(once, twice); +} + +#[test] +fn test_g2bfa3_lowercase_root_path_constant_transpiles_with_colons() { + let code = "fun main() {\n let m = i32::MAX\n println(m)\n}\n"; + let rust = squash(&transpile(code)); + assert!(rust.contains("i32::MAX"), "{rust}"); + assert_eq!(compiled_output(code, "fa3_max"), "2147483647\n"); +} + +// ---------------------------------------------- 4. user-defined `range` + +const USER_RANGE: &str = r" +fun range(a: i32, b: i32) -> Vec { + vec![a, b] +} +fun main() { + println(range(0, 5).count()) + println(range(0, 5).sum()) +} +"; + +#[test] +fn test_g2bfa4_user_defined_range_is_called_and_iterated() { + let rust = squash(&transpile(USER_RANGE)); + assert!(rust.contains("range(0,5).iter().count()"), "{rust}"); + assert_eq!(compiled_output(USER_RANGE, "fa4_user_range"), "2\n5\n"); +} + +#[test] +fn test_g2bfa4_builtin_range_count_still_direct() { + let code = "fun main() {\n let n = range(0, 4).count()\n println(n)\n}\n"; + assert_eq!(compiled_output(code, "fa4_builtin_range"), "4\n"); +} + +// --------------------------------------- 5. awaiting main return type + +#[test] +fn test_g2bfa5_async_main_with_result_return_type_compiles() { + let code = r" +async fun value() -> i32 { + 5 +} +async fun main() -> Result<(), String> { + let v = value().await + println(v) + Ok(()) +} +"; + assert_eq!(compiled_output(code, "fa5_result_main"), "5\n"); +} + +#[test] +fn test_g2bfa5_async_main_propagates_error_exit() { + let code = r#" +async fun check(x: i32) -> Result { + if x > 0 { Ok(x) } else { Err(String::from("neg")) } +} +async fun main() -> Result<(), String> { + let v = check(3).await? + println(v) + Ok(()) +} +"#; + assert_eq!(compiled_output(code, "fa5_try_main"), "3\n"); +} From f510331e6bf09dc43f70434b11bb551f9d6c1fbf Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 02:58:27 +0200 Subject: [PATCH 027/106] G2BFA3: the :: path marker is printed by fmt and decides the separator outside callee position Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/dispatcher.rs | 4 +++- .../expressions_helpers/field_access.rs | 22 +++++++++++++++++++ src/quality/formatter.rs | 13 ++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/backend/transpiler/dispatcher.rs b/src/backend/transpiler/dispatcher.rs index 39b95db4a..ae1b08378 100644 --- a/src/backend/transpiler/dispatcher.rs +++ b/src/backend/transpiler/dispatcher.rs @@ -239,7 +239,9 @@ impl Transpiler { self.transpile_struct_literal(name, fields, base.as_deref()) } ExprKind::ObjectLiteral { fields } => self.transpile_object_literal(fields), - ExprKind::FieldAccess { object, field } => self.transpile_field_access(object, field), + ExprKind::FieldAccess { object, field } => { + self.transpile_field_access_node(expr, object, field) + } ExprKind::IndexAccess { object, index } => self.transpile_index_access(object, index), ExprKind::Slice { object, start, end } => { self.transpile_slice(object, start.as_deref(), end.as_deref()) diff --git a/src/backend/transpiler/expressions_helpers/field_access.rs b/src/backend/transpiler/expressions_helpers/field_access.rs index 19302d2a6..fb29dbfea 100644 --- a/src/backend/transpiler/expressions_helpers/field_access.rs +++ b/src/backend/transpiler/expressions_helpers/field_access.rs @@ -77,6 +77,28 @@ impl Transpiler { .any(|prefix| name.starts_with(prefix)) } + /// Transpile a `FieldAccess` node. When the parser marked it as written with + /// `::` ([`Expr::is_path_access`]) it is a path segment (`i32::MAX`, + /// `helpers::double`); otherwise the separator is guessed from the object by + /// [`Self::transpile_field_access`]. + pub fn transpile_field_access_node( + &self, + access: &Expr, + object: &Expr, + field: &str, + ) -> Result { + let is_ident = field + .chars() + .next() + .is_some_and(|c| c.is_alphabetic() || c == '_'); + if !(access.is_path_access() && is_ident) { + return self.transpile_field_access(object, field); + } + let obj_tokens = self.transpile_expr(object)?; + let field_ident = format_ident!("{}", field); + Ok(quote! { #obj_tokens::#field_ident }) + } + pub fn transpile_field_access(&self, object: &Expr, field: &str) -> Result { use crate::frontend::ast::ExprKind; let obj_tokens = self.transpile_expr(object)?; diff --git a/src/quality/formatter.rs b/src/quality/formatter.rs index 1f9e26a24..485e67287 100644 --- a/src/quality/formatter.rs +++ b/src/quality/formatter.rs @@ -280,7 +280,7 @@ impl Formatter { } // Format the expression itself - let expr_str = self.format_expr_kind(&expr.kind, &indent_str, indent); + let expr_str = self.format_expr_node(expr, &indent_str, indent); // Append the formatted expression result.push_str(&expr_str); @@ -296,6 +296,17 @@ impl Formatter { // --- Extracted helpers for format_expr (CB-200 reduction) --- + /// RHLGA-1: a `FieldAccess` the parser marked as written with `::` is a path + /// segment; printing it with `.` would reparse as a method call or field. + fn format_expr_node(&self, expr: &Expr, indent_str: &str, indent: usize) -> String { + match &expr.kind { + ExprKind::FieldAccess { object, field } if expr.is_path_access() => { + format!("{}::{}", self.format_expr(object, indent), field) + } + kind => self.format_expr_kind(kind, indent_str, indent), + } + } + fn format_expr_kind(&self, kind: &ExprKind, indent_str: &str, indent: usize) -> String { match kind { ExprKind::Literal(lit) => self.format_literal_expr(lit), From 5f69e97a54dd0a31089ed7088c306d366e9250d1 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 03:01:13 +0200 Subject: [PATCH 028/106] G2BFA4: a user-defined range function shadows the builtin range; G2BFA5: an awaiting main keeps its declared return type; block_on park/wake is exercised Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/call_transpilation.rs | 9 +- src/backend/transpiler/function_transpiler.rs | 102 ++++++++++++++++-- src/backend/transpiler/utility_builtins.rs | 3 +- 3 files changed, 99 insertions(+), 15 deletions(-) diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index bed871fef..3ec2312bf 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -357,12 +357,14 @@ impl Transpiler { } /// RHLGA-1: a range receiver (`a..b`, `range(a, b)`) is already an iterator. - fn is_range_receiver(object: &Expr) -> bool { + /// `range(..)` is the builtin only when the program defines no `range` function. + fn is_range_receiver(&self, object: &Expr) -> bool { match &object.kind { ExprKind::Range { .. } => true, ExprKind::Call { func, args } => { matches!(&func.kind, ExprKind::Identifier(name) if name == "range") && matches!(args.len(), 1 | 2) + && !self.function_signatures.contains_key("range") } _ => false, } @@ -382,12 +384,13 @@ impl Transpiler { /// min/max); a range is called directly, parenthesised when it is a /// range literal so the method binds to the whole range. fn transpile_collection_aggregate( + &self, obj_tokens: &TokenStream, method: &str, method_ident: &proc_macro2::Ident, object: &Expr, ) -> TokenStream { - let (iter, copied) = if Self::is_range_receiver(object) { + let (iter, copied) = if self.is_range_receiver(object) { let range = Self::parenthesize_range_literal(obj_tokens, object); (range.clone(), range) } else { @@ -439,7 +442,7 @@ impl Transpiler { if Transpiler::is_dataframe_expr(object) { self.transpile_dataframe_method(object, method, &[]) } else { - Ok(Self::transpile_collection_aggregate( + Ok(self.transpile_collection_aggregate( obj_tokens, method, method_ident, diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index 5eabf86b9..346ad7749 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -97,7 +97,7 @@ impl Transpiler { // RHLGA-1: rustc rejects `async fn main` (E0752) and `.await` in a sync fn (E0728). let (body_tokens, is_async) = - self.adapt_awaiting_main(name, body, body_tokens, is_async)?; + self.adapt_awaiting_main(name, body, body_tokens, is_async, effective_return_type)?; let return_type_tokens = if needs_lifetime { self.generate_return_type_tokens_with_lifetime(name, effective_return_type, body)? @@ -205,9 +205,10 @@ impl Transpiler { body: &Expr, body_tokens: TokenStream, is_async: bool, + return_type: Option<&Type>, ) -> Result<(TokenStream, bool)> { if name == "main" && (is_async || Self::tokens_contain_await(&body_tokens)) { - Ok((self.generate_block_on_main_body(body)?, false)) + Ok((self.generate_block_on_main_body(body, return_type)?, false)) } else { Ok((body_tokens, is_async)) } @@ -226,16 +227,49 @@ impl Transpiler { /// Body of a `main` that awaits: the original body runs as an `async move` block driven /// by a thread-parking `block_on` (std only, no unsafe); a non-unit tail is printed. /// Complexity: 2 - fn generate_block_on_main_body(&self, body: &Expr) -> Result { + fn generate_block_on_main_body( + &self, + body: &Expr, + return_type: Option<&Type>, + ) -> Result { let inner = self.generate_body_tokens(body, true)?; - let run = quote! { __ruchy_block_on(async move { #inner }) }; - let tail = if Self::has_non_unit_last_expr(body) { - quote! { let __ruchy_main_value = #run; println!("{:?}", __ruchy_main_value); } - } else { - quote! { #run; } + let tail = match self.declared_non_unit_type(return_type)? { + // RHLGA-1: a declared return type is main's value; the typed binding pins the + // async block's output so `?` inside it resolves + Some(ret) => quote! { + let __ruchy_main_value: #ret = __ruchy_block_on(async move { #inner }); + __ruchy_main_value + }, + None => { + let run = quote! { __ruchy_block_on(async move { #inner }) }; + if Self::has_non_unit_last_expr(body) { + quote! { let __ruchy_main_value = #run; println!("{:?}", __ruchy_main_value); } + } else { + quote! { #run; } + } + } }; + let block_on = Self::block_on_fn_tokens(); Ok(quote! { { + #block_on + #tail + } + }) + } + + /// Tokens of a declared return type other than `()`. + fn declared_non_unit_type(&self, return_type: Option<&Type>) -> Result> { + let Some(ty) = return_type else { + return Ok(None); + }; + let tokens = self.transpile_type(ty)?; + Ok((tokens.to_string().replace(' ', "") != "()").then_some(tokens)) + } + + /// A thread-parking `block_on` (std only, no unsafe) for the awaiting `main`. + fn block_on_fn_tokens() -> TokenStream { + quote! { fn __ruchy_block_on(fut: F) -> F::Output { struct ThreadWaker(::std::thread::Thread); impl ::std::task::Wake for ThreadWaker { @@ -255,9 +289,7 @@ impl Transpiler { ::std::thread::park(); } } - #tail - } - }) + } } } @@ -270,6 +302,54 @@ mod tests { Transpiler::new() } + /// RHLGA-1: the emitted `block_on` parks until woken. The future is Pending on its + /// first poll and is woken from another thread, so the park/wake path runs. + #[test] + fn test_rhlga1_block_on_parks_until_a_pending_future_is_woken() { + let block_on = Transpiler::block_on_fn_tokens(); + let program = format!( + "{block_on}\n{}", + r#" +struct WakeLater { polls: u32 } +impl std::future::Future for WakeLater { + type Output = u32; + fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll { + self.polls += 1; + if self.polls > 1 { + return std::task::Poll::Ready(self.polls); + } + let waker = cx.waker().clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(20)); + waker.wake(); + }); + std::task::Poll::Pending + } +} +fn main() { + println!("{}", __ruchy_block_on(WakeLater { polls: 0 })); +} +"# + ); + let dir = std::env::temp_dir().join(format!("rhlga1_block_on_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("scratch dir"); + let (src, bin) = (dir.join("main.rs"), dir.join("main")); + std::fs::write(&src, program).expect("write rust"); + let out = std::process::Command::new("rustc") + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&src) + .output() + .expect("rustc runs"); + assert!( + out.status.success(), + "{}", + String::from_utf8_lossy(&out.stderr) + ); + let run = std::process::Command::new(&bin).output().expect("runs"); + assert_eq!(String::from_utf8_lossy(&run.stdout), "2\n"); + } + #[test] fn test_transpile_simple_function() { let mut transpiler = create_transpiler(); diff --git a/src/backend/transpiler/utility_builtins.rs b/src/backend/transpiler/utility_builtins.rs index 760a6e0b0..6ccb80d8b 100644 --- a/src/backend/transpiler/utility_builtins.rs +++ b/src/backend/transpiler/utility_builtins.rs @@ -165,7 +165,8 @@ impl Transpiler { base_name: &str, args: &[Expr], ) -> Result> { - if base_name != "range" { + // RHLGA-1: a user-defined `range` function shadows the builtin + if base_name != "range" || self.function_signatures.contains_key("range") { return Ok(None); } From ceec1a409fbed8c659cb59ba8fe0d9be1fc6b087 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 03:14:48 +0200 Subject: [PATCH 029/106] G2BFB3: test scratch dirs are TempDirs; rustc output stays out of /tmp and the crate root; the CLI verb walk is hermetic - g2b_t3_misc, g2b_t2a_receiver_and_paths: scratch_dir returns a TempDir - quality_001_self_receiver: rustc_lib compiles in a TempDir with --out-dir (no fixed /tmp paths, no .rlib in the crate root) - g2b_s2_cli: every leaf verb's --help must exit 0 without the stub line; only offline-safe verbs (parse, check, transpile, ast, lint, fmt, score, tier, provability, prove, explain) run for real; a timeout fails Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/g2b_s2_cli.rs | 107 +++++++++++++++++++--------- tests/g2b_t2a_receiver_and_paths.rs | 17 +++-- tests/g2b_t3_misc.rs | 17 +++-- tests/quality_001_self_receiver.rs | 39 +++++----- 4 files changed, 110 insertions(+), 70 deletions(-) diff --git a/tests/g2b_s2_cli.rs b/tests/g2b_s2_cli.rs index 9e2ff5e95..4f1890e34 100644 --- a/tests/g2b_s2_cli.rs +++ b/tests/g2b_s2_cli.rs @@ -4,8 +4,11 @@ //! The command router used to end in `_ => { eprintln!("Command not yet //! implemented"); Ok(()) }`, so any `Commands` variant it did not route (e.g. //! `prove`) printed that line and exited 0. These tests walk every verb that -//! `ruchy --help` lists (recursing into nested command groups) and run it on a -//! trivial input: it must either work or exit non-zero. +//! `ruchy --help` lists (recursing into nested command groups): each verb's +//! `--help` must exit 0 without the stub line, and each verb that is safe to run +//! offline on a trivial file is run on one: it must either work or exit non-zero, +//! and a verb still running at the timeout fails. Servers, REPLs, network and +//! package verbs are not run for real, so the test is hermetic. use assert_cmd::Command; use std::path::Path; @@ -58,27 +61,62 @@ fn leaf_verbs(prefix: &[String]) -> Vec> { .collect() } -/// `None` when the verb works (exit 0 without the stub marker), is still -/// running at the timeout (servers, REPLs), or fails loudly (non-zero exit). -/// `Some(report)` when it exits 0 while printing the stub marker. +/// Verbs that run offline on a trivial file, without servers, network, a +/// registry or a REPL; only these are run for real. +const SAFE_OFFLINE_VERBS: [&str; 11] = [ + "parse", + "check", + "transpile", + "ast", + "lint", + "fmt", + "score", + "tier", + "provability", + "prove", + "explain", +]; + +/// Combined stdout and stderr of a finished command. +fn output_text(output: &std::process::Output) -> String { + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) +} + +/// `Some(report)` when `ruchy --help` fails or prints the stub marker. +fn broken_help(verb: &[String]) -> Option { + let output = ruchy_cmd() + .args(verb) + .arg("--help") + .timeout(PER_VERB_TIMEOUT) + .output(); + let Ok(output) = output else { + return Some(format!("`ruchy {} --help` timed out", verb.join(" "))); + }; + let stub = output_text(&output).to_lowercase().contains(STUB_MARKER); + (!output.status.success() || stub) + .then(|| format!("`ruchy {} --help` failed or is a stub", verb.join(" "))) +} + +/// `None` when the verb works (exit 0 without the stub marker) or fails +/// loudly (non-zero exit). `Some(report)` when it exits 0 while printing the +/// stub marker, or is still running at the timeout. fn silent_stub(verb: &[String], dir: &Path) -> Option { - let output = match ruchy_cmd() + let output = ruchy_cmd() .current_dir(dir) .args(verb) .arg("main.ruchy") .write_stdin("") .timeout(PER_VERB_TIMEOUT) - .output() - { - Ok(output) => output, - Err(_) => return None, + .output(); + let Ok(output) = output else { + return Some(format!("`ruchy {}` timed out", verb.join(" "))); }; - let text = format!( - "{}{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - let is_stub = output.status.success() && text.to_lowercase().contains(STUB_MARKER); + let is_stub = + output.status.success() && output_text(&output).to_lowercase().contains(STUB_MARKER); is_stub.then(|| { format!( "`ruchy {}` exited 0 printing {STUB_MARKER:?}", @@ -105,29 +143,30 @@ fn test_g2b_s2_help_lists_the_expected_verbs() { } #[test] -fn test_g2b_s2_no_verb_silently_succeeds_as_a_stub() { +fn test_g2b_s2_every_verb_has_a_working_help() { let verbs = leaf_verbs(&[]); assert!( verbs.len() > 40, "expected the full verb tree, got {verbs:?}" ); - let failures: Vec = std::thread::scope(|scope| { - let handles: Vec<_> = verbs - .chunks(verbs.len().div_ceil(8)) - .map(|chunk| { - scope.spawn(move || { - chunk - .iter() - .filter_map(|verb| silent_stub(verb, scratch_dir().path())) - .collect::>() - }) - }) - .collect(); - handles - .into_iter() - .flat_map(|h| h.join().expect("verb worker panicked")) - .collect() - }); + let failures: Vec = verbs.iter().filter_map(|verb| broken_help(verb)).collect(); + assert!(failures.is_empty(), "broken help:\n{}", failures.join("\n")); +} + +#[test] +fn test_g2b_s2_no_safe_verb_silently_succeeds_as_a_stub() { + let verbs = leaf_verbs(&[]); + let safe: Vec> = SAFE_OFFLINE_VERBS + .iter() + .map(|name| vec![(*name).to_string()]) + .collect(); + for verb in &safe { + assert!(verbs.contains(verb), "`ruchy --help` should list {verb:?}"); + } + let failures: Vec = safe + .iter() + .filter_map(|verb| silent_stub(verb, scratch_dir().path())) + .collect(); assert!( failures.is_empty(), "silent stubs:\n{}", diff --git a/tests/g2b_t2a_receiver_and_paths.rs b/tests/g2b_t2a_receiver_and_paths.rs index 92e147c1e..390fa8b59 100644 --- a/tests/g2b_t2a_receiver_and_paths.rs +++ b/tests/g2b_t2a_receiver_and_paths.rs @@ -14,16 +14,18 @@ use ruchy::{Parser, Transpiler}; use std::path::Path; use std::process::Command; -fn scratch_dir(name: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("g2b_t2a_{}_{name}", std::process::id())); - std::fs::create_dir_all(&dir).expect("scratch dir"); - dir +/// A scratch directory for one program, removed when the returned guard drops. +fn scratch_dir(name: &str) -> tempfile::TempDir { + tempfile::Builder::new() + .prefix(&format!("g2b_t2a_{name}_")) + .tempdir() + .expect("scratch dir") } fn compile_and_run(rust: &str, name: &str) -> String { let dir = scratch_dir(name); - let src = dir.join("main.rs"); - let bin = dir.join("main"); + let src = dir.path().join("main.rs"); + let bin = dir.path().join("main"); std::fs::write(&src, rust).expect("write rust"); let out = Command::new("rustc") .args(["--edition", "2021", "-A", "warnings", "-o"]) @@ -49,7 +51,8 @@ fn transpile(ast: &Expr) -> String { } fn ruchy_run(code: &str, name: &str) -> String { - let file = scratch_dir(name).join("prog.ruchy"); + let dir = scratch_dir(name); + let file = dir.path().join("prog.ruchy"); std::fs::write(&file, code).expect("write ruchy"); let out = Command::new(env!("CARGO_BIN_EXE_ruchy")) .arg("run") diff --git a/tests/g2b_t3_misc.rs b/tests/g2b_t3_misc.rs index ce441b8d9..d857f59cd 100644 --- a/tests/g2b_t3_misc.rs +++ b/tests/g2b_t3_misc.rs @@ -13,10 +13,12 @@ use ruchy::{Parser, Transpiler}; use std::process::Command; -fn scratch_dir(name: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("g2b_t3_{}_{name}", std::process::id())); - std::fs::create_dir_all(&dir).expect("scratch dir"); - dir +/// A scratch directory for one program, removed when the returned guard drops. +fn scratch_dir(name: &str) -> tempfile::TempDir { + tempfile::Builder::new() + .prefix(&format!("g2b_t3_{name}_")) + .tempdir() + .expect("scratch dir") } fn transpile(code: &str) -> String { @@ -30,8 +32,8 @@ fn transpile(code: &str) -> String { fn compiled_output(code: &str, name: &str) -> String { let rust = transpile(code); let dir = scratch_dir(name); - let src = dir.join("main.rs"); - let bin = dir.join("main"); + let src = dir.path().join("main.rs"); + let bin = dir.path().join("main"); std::fs::write(&src, &rust).expect("write rust"); let out = Command::new("rustc") .args(["--edition", "2021", "-A", "warnings", "-o"]) @@ -50,7 +52,8 @@ fn compiled_output(code: &str, name: &str) -> String { } fn interpreted_output(code: &str, name: &str) -> String { - let file = scratch_dir(name).join("prog.ruchy"); + let dir = scratch_dir(name); + let file = dir.path().join("prog.ruchy"); std::fs::write(&file, code).expect("write ruchy"); let out = Command::new(env!("CARGO_BIN_EXE_ruchy")) .arg("run") diff --git a/tests/quality_001_self_receiver.rs b/tests/quality_001_self_receiver.rs index 7171ef458..a2cb10bec 100644 --- a/tests/quality_001_self_receiver.rs +++ b/tests/quality_001_self_receiver.rs @@ -15,6 +15,19 @@ use ruchy::{Parser, Transpiler}; +/// Compile `rust_code` as a library with rustc inside a scratch directory, so +/// neither the source nor the `.rlib` lands in `/tmp` or the crate root. +fn rustc_lib(rust_code: &str) -> std::io::Result { + let dir = tempfile::tempdir()?; + let src = dir.path().join("lib.rs"); + std::fs::write(&src, rust_code)?; + std::process::Command::new("rustc") + .args(["--crate-type", "lib", "--out-dir"]) + .arg(dir.path()) + .arg(&src) + .output() +} + /// Test 1: Basic &self method - should preserve reference #[test] fn test_quality_001_01_immutable_self_reference() { @@ -88,12 +101,7 @@ pub fn test() -> String { let rust_code = result.unwrap().to_string(); // Verify rustc compilation succeeds - std::fs::write("/tmp/quality_001_02_output.rs", &rust_code).expect("Failed to write test file"); - - let rustc_result = std::process::Command::new("rustc") - .args(["--crate-type", "lib", "/tmp/quality_001_02_output.rs"]) - .output() - .expect("Failed to run rustc"); + let rustc_result = rustc_lib(&rust_code).expect("Failed to run rustc"); if !rustc_result.status.success() { let stderr = String::from_utf8_lossy(&rustc_result.stderr); @@ -258,12 +266,7 @@ impl LambdaRuntime { ); // Verify it compiles - std::fs::write("/tmp/quality_001_06_output.rs", &rust_code).expect("Failed to write test file"); - - let rustc_result = std::process::Command::new("rustc") - .args(["--crate-type", "lib", "/tmp/quality_001_06_output.rs"]) - .output() - .expect("Failed to run rustc"); + let rustc_result = rustc_lib(&rust_code).expect("Failed to run rustc"); if !rustc_result.status.success() { let stderr = String::from_utf8_lossy(&rustc_result.stderr); @@ -489,13 +492,8 @@ mod property_tests { let rust_code = result.unwrap().to_string(); - // Write to temp file and verify rustc compilation - let temp_file = format!("/tmp/quality_001_prop_{struct_name}.rs"); - std::fs::write(&temp_file, &rust_code).ok(); - - let rustc_result = std::process::Command::new("rustc") - .args(["--crate-type", "lib", &temp_file]) - .output(); + // Verify rustc compilation in a scratch directory + let rustc_result = rustc_lib(&rust_code); prop_assume!(rustc_result.is_ok()); let output = rustc_result.unwrap(); @@ -506,9 +504,6 @@ mod property_tests { "Property violation: Multiple &self calls cause move error:\n{}", String::from_utf8_lossy(&output.stderr) ); - - // Cleanup - std::fs::remove_file(&temp_file).ok(); } /// Property 5: Mixed receiver types in same impl block From 9a2302b40d986d70f66b7ce694c0362ce5b6042a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 03:16:39 +0200 Subject: [PATCH 030/106] G2BFB4: stale comments describe the fixed defects; t5_04 parses (h.f)(3) from source; is_path_access no longer steals Expr::new's docs - g2b_t2a t5_02: nested pub mod parses and transpiles; only std::cmp keeps it transpiler-only - g2b_t2a t5_04: the program calls (h.f)(3) directly; retarget_probe removed - ast.rs: is_path_access moved above Expr::new's doc block Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/frontend/ast.rs | 16 ++++++------- tests/g2b_t2a_receiver_and_paths.rs | 35 +++++------------------------ 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/frontend/ast.rs b/src/frontend/ast.rs index a56e6afdd..a29c7b00e 100644 --- a/src/frontend/ast.rs +++ b/src/frontend/ast.rs @@ -281,6 +281,14 @@ pub struct Expr { pub const PATH_ACCESS_MARKER: &str = "::"; impl Expr { + /// True when this expression is a `FieldAccess` written with `::` (a path + /// segment), false for `obj.field` and every other expression. + #[must_use] + pub fn is_path_access(&self) -> bool { + matches!(self.kind, ExprKind::FieldAccess { .. }) + && self.attributes.iter().any(|a| a.name == PATH_ACCESS_MARKER) + } + /// Creates a new expression with the given kind and span. /// /// This is the primary constructor for building AST nodes. The expression @@ -301,14 +309,6 @@ impl Expr { /// Span::new(0, 4) /// ); /// ``` - /// True when this expression is a `FieldAccess` written with `::` (a path - /// segment), false for `obj.field` and every other expression. - #[must_use] - pub fn is_path_access(&self) -> bool { - matches!(self.kind, ExprKind::FieldAccess { .. }) - && self.attributes.iter().any(|a| a.name == PATH_ACCESS_MARKER) - } - #[must_use] pub fn new(kind: ExprKind, span: Span) -> Self { Self { diff --git a/tests/g2b_t2a_receiver_and_paths.rs b/tests/g2b_t2a_receiver_and_paths.rs index 390fa8b59..88f9b67fc 100644 --- a/tests/g2b_t2a_receiver_and_paths.rs +++ b/tests/g2b_t2a_receiver_and_paths.rs @@ -9,7 +9,7 @@ #![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] -use ruchy::frontend::ast::{Expr, ExprKind}; +use ruchy::frontend::ast::Expr; use ruchy::{Parser, Transpiler}; use std::path::Path; use std::process::Command; @@ -87,9 +87,8 @@ fun main() { } /// A three-segment path `std::cmp::max(..)`, compiled with rustc. The -/// interpreter has no `std::cmp` (`Object has no field named 'cmp'`) and a -/// nested `pub mod`/`pub struct` inside a `mod` does not parse or transpile, so -/// this case is transpiler-only. +/// interpreter has no `std::cmp` (`Object has no field named 'cmp'`), so this +/// case is transpiler-only. #[test] fn test_g2b_t5_02_nested_path_call() { let code = r#" @@ -119,46 +118,22 @@ fun main() { assert!(rust.contains("Point :: new"), "{rust}"); } -/// Replace the callee `probe` of every Call with `object.field` (a `.` access). -fn retarget_probe(expr: &mut Expr, object: &str, field: &str) { - if let ExprKind::Call { func, .. } = &mut expr.kind { - if matches!(&func.kind, ExprKind::Identifier(n) if n == "probe") { - let obj = Expr::new(ExprKind::Identifier(object.to_string()), func.span); - let access = ExprKind::FieldAccess { - object: Box::new(obj), - field: field.to_string(), - }; - **func = Expr::new(access, func.span); - } - } - match &mut expr.kind { - ExprKind::Block(items) => items - .iter_mut() - .for_each(|e| retarget_probe(e, object, field)), - ExprKind::Function { body, .. } => retarget_probe(body, object, field), - _ => {} - } -} - /// A field holding a fn pointer, called through `(h.f)(3)`. The callee is a /// `.` FieldAccess (not a path), so Rust needs `(h.f)(3)`, not `h::f(3)`. -/// The AST is built from source with a `probe` callee retargeted to `h.f` -/// because the parser splits `(h.f)(3)` inside a block (FieldAccess span 0..0). #[test] fn test_g2b_t5_04_field_callee_is_not_a_path() { let code = r#" struct Holder { f: fn(i32) -> i32 } fun double(x: i32) -> i32 { x * 2 } fun apply(h: Holder) -> i32 { - probe(3) + (h.f)(3) } fun main() { let h = Holder { f: double } println!("{}", apply(h)) } "#; - let mut ast = Parser::new(code).parse().expect("parses"); - retarget_probe(&mut ast, "h", "f"); + let ast = Parser::new(code).parse().expect("parses"); let rust = transpile(&ast); assert!( !rust.contains("h :: f"), From c6a989352b7ccdb0ab42449efefedec9752b56bc Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 03:19:12 +0200 Subject: [PATCH 031/106] G2BFB5: a nested module keeps its recorded pub(crate)/pub(super) visibility - transpile_module_item emits module_visibility(expr): pub, pub(crate), pub(super) from the parser's pub attribute args, nothing when private - tests/g2b_fix_b.rs: 01/02 RED (bare pub mod), 03 plain pub, 04 rustc run Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/program_transpiler.rs | 26 +++++-- tests/g2b_fix_b.rs | 81 ++++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 tests/g2b_fix_b.rs diff --git a/src/backend/transpiler/program_transpiler.rs b/src/backend/transpiler/program_transpiler.rs index fc1716623..a6e5c5371 100644 --- a/src/backend/transpiler/program_transpiler.rs +++ b/src/backend/transpiler/program_transpiler.rs @@ -260,19 +260,16 @@ impl Transpiler { /// Transpile one item of a module body as a Rust item. /// /// A nested module is emitted as an item (`pub mod b { .. }` when marked - /// `pub`), not as an expression whose block body is not an item (NESTEDMOD-1). + /// `pub`, `pub(crate)`/`pub(super)` as recorded), not as an expression whose + /// block body is not an item (NESTEDMOD-1). /// Complexity: 3 (within Toyota Way limits) fn transpile_module_item(&self, expr: &Expr) -> Result { match &expr.kind { ExprKind::Function { .. } => self.transpile_function_expr(expr), ExprKind::Module { name, body } => { let module = self.transpile_module_declaration(name, body)?; - let is_pub = expr.attributes.iter().any(|a| a.name == "pub"); - Ok(if is_pub { - quote! { pub #module } - } else { - module - }) + let visibility = module_visibility(expr); + Ok(quote! { #visibility #module }) } _ => self.transpile_expr(expr), } @@ -670,6 +667,21 @@ impl Transpiler { // Tests // ============================================================================ +/// The visibility the parser recorded on a module item: `pub`, `pub(crate)`, +/// `pub(super)`, or nothing for a private module. `pub(in path)` records no +/// argument and is emitted as `pub`. +/// Complexity: 3 (within Toyota Way limits) +fn module_visibility(expr: &Expr) -> TokenStream { + let Some(attr) = expr.attributes.iter().find(|a| a.name == "pub") else { + return TokenStream::new(); + }; + match attr.args.first().map(String::as_str) { + Some("crate") => quote! { pub(crate) }, + Some("super") => quote! { pub(super) }, + _ => quote! { pub }, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/g2b_fix_b.rs b/tests/g2b_fix_b.rs new file mode 100644 index 000000000..d145e27b7 --- /dev/null +++ b/tests/g2b_fix_b.rs @@ -0,0 +1,81 @@ +//! G2BFB (RHLGA-1): review fixes on the G2 part 2 branch. +//! +//! Item 5: a nested module keeps the visibility the parser recorded +//! (`pub(crate)`, `pub(super)`), instead of widening every `pub(..)` to `pub`. +//! The program is transpiled, compiled with rustc and run. + +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] + +use ruchy::{Parser, Transpiler}; +use std::process::Command; + +fn transpile(code: &str) -> String { + let ast = Parser::new(code).parse().expect("parses"); + Transpiler::new() + .transpile_to_program(&ast) + .expect("transpiles") + .to_string() +} + +fn compile_and_run(rust: &str) -> String { + let dir = tempfile::tempdir().expect("scratch dir"); + let src = dir.path().join("main.rs"); + let bin = dir.path().join("main"); + std::fs::write(&src, rust).expect("write rust"); + let out = Command::new("rustc") + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&src) + .output() + .expect("rustc runs"); + assert!( + out.status.success(), + "rustc rejected the transpiled program:\n{}\n--- rust ---\n{rust}", + String::from_utf8_lossy(&out.stderr) + ); + let run = Command::new(&bin).output().expect("binary runs"); + assert!(run.status.success(), "binary failed: {run:?}"); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +const NESTED_VISIBILITY: &str = r#" +mod outer { + pub(crate) mod inner { + pub fun value() -> i32 { 7 } + } + pub(super) mod up { + pub fun value() -> i32 { 5 } + } + pub mod open { + pub fun value() -> i32 { 1 } + } +} +fun main() { + println!("{}", outer::inner::value() + outer::up::value() + outer::open::value()) +} +"#; + +#[test] +fn test_g2bfb5_01_nested_pub_crate_module_keeps_its_visibility() { + let rust = transpile(NESTED_VISIBILITY); + assert!(rust.contains("pub (crate) mod inner"), "{rust}"); +} + +#[test] +fn test_g2bfb5_02_nested_pub_super_module_keeps_its_visibility() { + let rust = transpile(NESTED_VISIBILITY); + assert!(rust.contains("pub (super) mod up"), "{rust}"); +} + +#[test] +fn test_g2bfb5_03_nested_plain_pub_module_stays_pub() { + let rust = transpile(NESTED_VISIBILITY); + assert!(rust.contains("pub mod open"), "{rust}"); +} + +#[test] +fn test_g2bfb5_04_nested_module_visibilities_compile_and_run() { + let rust = transpile(NESTED_VISIBILITY); + assert_eq!(compile_and_run(&rust).trim(), "13", "{rust}"); +} From 88d58243535d8f3ecc31fe805cb69feb5d4584bc Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 03:27:12 +0200 Subject: [PATCH 032/106] G2BFA2: mutation_detection tests assert an assignment mutates its target wherever it appears The two tests pinned the narrower walk: `y = (x = 1)` and `return (x = 1)` do assign x. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/mutation_detection.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backend/transpiler/mutation_detection.rs b/src/backend/transpiler/mutation_detection.rs index 7117d701e..bcbb9a0db 100644 --- a/src/backend/transpiler/mutation_detection.rs +++ b/src/backend/transpiler/mutation_detection.rs @@ -822,8 +822,9 @@ mod tests { #[test] fn test_assign_nested_value() { let nested_assign = assign(ident("y"), assign(ident("x"), int_lit(1))); - // Note: Assignment in value position doesn't mutate x as target - assert!(!is_variable_mutated("x", &nested_assign)); + // `y = (x = 1)` assigns x: an assignment mutates its target wherever it + // appears (G2BFA2; this test used to pin the narrower walk that missed it). + assert!(is_variable_mutated("x", &nested_assign)); } #[test] @@ -837,7 +838,7 @@ mod tests { let ret = make_expr(ExprKind::Return { value: Some(Box::new(assign(ident("x"), int_lit(1)))), }); - // Return value is evaluated but doesn't count as mutation - assert!(!is_variable_mutated("x", &ret)); + // `return (x = 1)` assigns x before returning (G2BFA2). + assert!(is_variable_mutated("x", &ret)); } } From 28bfffdfa845829c6e1c5288b14e970a4a2a5529 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 03:37:52 +0200 Subject: [PATCH 033/106] RAWIDENT-1: a function or method named with a Rust reserved word is emitted as a raw identifier at definition and call quality_001's saved case (getter named `do`) produced `fn do(&self)`, which rustc rejects. Its text-match now also accepts the raw spelling. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/block_transpiler.rs | 4 +- src/backend/transpiler/call_transpilation.rs | 4 +- src/backend/transpiler/expr_dispatcher.rs | 16 ++ src/backend/transpiler/function_transpiler.rs | 6 +- src/backend/transpiler/types.rs | 27 ++-- tests/quality_001_self_receiver.rs | 12 +- tests/rawident_1_rust_keywords.rs | 141 ++++++++++++++++++ 7 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 tests/rawident_1_rust_keywords.rs diff --git a/src/backend/transpiler/block_transpiler.rs b/src/backend/transpiler/block_transpiler.rs index b76c421f0..e3910086e 100644 --- a/src/backend/transpiler/block_transpiler.rs +++ b/src/backend/transpiler/block_transpiler.rs @@ -9,7 +9,7 @@ use super::Transpiler; use crate::frontend::ast::{Expr, ExprKind, PipelineStage}; use anyhow::Result; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; impl Transpiler { /// Transpile block expressions with smart brace handling @@ -88,7 +88,7 @@ impl Transpiler { Ok(quote! { #func_tokens(#prev #(, #arg_tokens)*) }) } ExprKind::MethodCall { method, args, .. } => { - let method_ident = format_ident!("{}", method); + let method_ident = Transpiler::safe_ident(method); // RAWIDENT-1 let arg_tokens: Result> = args.iter().map(|a| self.transpile_expr(a)).collect(); let arg_tokens = arg_tokens?; diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index 3ec2312bf..172b6087b 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -313,7 +313,7 @@ impl Transpiler { if let ExprKind::Identifier(name) = &object.kind { if self.module_names.contains(name) { let module_ident = format_ident!("{}", name); - let method_ident = format_ident!("{}", method); + let method_ident = Transpiler::safe_ident(method); // RAWIDENT-1 let arg_tokens: Result> = args.iter().map(|a| self.transpile_expr(a)).collect(); let arg_tokens = arg_tokens?; @@ -322,7 +322,7 @@ impl Transpiler { } let obj_tokens = self.transpile_expr(object)?; - let method_ident = format_ident!("{}", method); + let method_ident = Transpiler::safe_ident(method); // RAWIDENT-1 let arg_tokens: Result> = args.iter().map(|a| self.transpile_expr(a)).collect(); let arg_tokens = arg_tokens?; diff --git a/src/backend/transpiler/expr_dispatcher.rs b/src/backend/transpiler/expr_dispatcher.rs index ecb6a7020..c657a2e8f 100644 --- a/src/backend/transpiler/expr_dispatcher.rs +++ b/src/backend/transpiler/expr_dispatcher.rs @@ -72,6 +72,22 @@ impl Transpiler { ) } + /// RAWIDENT-1: A function/method name that collides with a Rust reserved + /// word (but not a Ruchy keyword, e.g. `do`, `box`, `typeof`) must be + /// emitted as a raw identifier so rustc accepts the definition/call. + /// `self`, `Self`, `super`, `crate`, `_` can never be raw identifiers; + /// they are returned verbatim (Ruchy never emits them as fn/method names + /// anyway, but the check keeps this helper safe to call on any name). + /// Complexity: 2 (within Toyota Way limits) + pub(crate) fn safe_ident(name: &str) -> proc_macro2::Ident { + let cannot_be_raw = matches!(name, "self" | "Self" | "super" | "crate" | "_"); + if !cannot_be_raw && Self::is_rust_reserved_keyword(name) { + proc_macro2::Ident::new_raw(name, proc_macro2::Span::call_site()) + } else { + proc_macro2::Ident::new(name, proc_macro2::Span::call_site()) + } + } + /// Main expression transpilation dispatcher /// /// Routes expressions to specialized handlers based on ExprKind. diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index 346ad7749..0dc47c2ec 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -6,7 +6,7 @@ use crate::frontend::ast::{Expr, ExprKind, Param, Type, TypeKind}; use anyhow::Result; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; use super::Transpiler; @@ -24,7 +24,9 @@ impl Transpiler { is_pub: bool, attributes: &[crate::frontend::ast::Attribute], ) -> Result { - let fn_name = format_ident!("{}", name); + // RAWIDENT-1: a Rust-reserved-but-not-Ruchy-keyword name (e.g. `do`, `box`, + // `typeof`) must be emitted as a raw identifier so rustc accepts the fn. + let fn_name = Transpiler::safe_ident(name); // Check if we need to add lifetime parameter let needs_lifetime = super::type_analysis::needs_lifetime_parameter(params, return_type); diff --git a/src/backend/transpiler/types.rs b/src/backend/transpiler/types.rs index 4f3f7135a..5cbc6f92a 100644 --- a/src/backend/transpiler/types.rs +++ b/src/backend/transpiler/types.rs @@ -823,10 +823,11 @@ impl Transpiler { } else { quote! {} }; + // RAWIDENT-1: a named constructor may collide with a reserved word. let method_name = ctor .name .as_ref() - .map_or_else(|| format_ident!("new"), |n| format_ident!("{}", n)); + .map_or_else(|| format_ident!("new"), |n| Transpiler::safe_ident(n)); let return_type = if let Some(ref ret_ty) = ctor.return_type { let ret_tokens = self.transpile_type(ret_ty)?; quote! { -> #ret_tokens } @@ -852,7 +853,7 @@ impl Transpiler { methods .iter() .map(|method| { - let method_name = format_ident!("{}", method.name); + let method_name = Transpiler::safe_ident(&method.name); // RAWIDENT-1 let params = self.transpile_params(&method.params)?; let return_type = if let Some(ref ret_ty) = method.return_type { let ret_tokens = self.transpile_type(ret_ty)?; @@ -1166,8 +1167,8 @@ impl Transpiler { method: &TraitMethod, mutating: &HashSet, ) -> Result { - let method_name = format_ident!("{}", method.name); - // TRANSPILER-TRAIT-001: bare `self` in a default method that mutates it is `&mut self` + let method_name = Transpiler::safe_ident(&method.name); // RAWIDENT-1 + // TRANSPILER-TRAIT-001: bare `self` in a default method that mutates it is `&mut self` let self_is_mutated = mutating.contains(&method.name); let param_tokens: Vec = method .params @@ -1236,11 +1237,11 @@ impl Transpiler { let method_tokens: Result> = methods .iter() .map(|method| { - let method_name = format_ident!("{}", method.name); - // TRANSPILER-METHOD-SELF-001 FIX: Check if self is mutated in method body - // to infer &mut self vs &self when not explicitly annotated - // RHLGA-1: bare `self` also mutates through nested/indexed fields, mutating - // std methods on a field and calls to mutating sibling methods (fixpoint) + let method_name = Transpiler::safe_ident(&method.name); // RAWIDENT-1 + // TRANSPILER-METHOD-SELF-001 FIX: Check if self is mutated in method body + // to infer &mut self vs &self when not explicitly annotated + // RHLGA-1: bare `self` also mutates through nested/indexed fields, mutating + // std methods on a field and calls to mutating sibling methods (fixpoint) let self_is_mutated = impl_self_is_mutated(method, &mutating); // Process parameters let param_tokens: Vec = method @@ -1385,8 +1386,8 @@ impl Transpiler { let trait_method_tokens: Result> = methods .iter() .map(|method| { - let method_name = format_ident!("{}", method.name); - // Process parameters + let method_name = Transpiler::safe_ident(&method.name); // RAWIDENT-1 + // Process parameters let param_tokens: Vec = method .params .iter() @@ -1421,8 +1422,8 @@ impl Transpiler { let impl_method_tokens: Result> = methods .iter() .map(|method| { - let method_name = format_ident!("{}", method.name); - // Process parameters (same as trait) + let method_name = Transpiler::safe_ident(&method.name); // RAWIDENT-1 + // Process parameters (same as trait) let param_tokens: Vec = method .params .iter() diff --git a/tests/quality_001_self_receiver.rs b/tests/quality_001_self_receiver.rs index a2cb10bec..94475c098 100644 --- a/tests/quality_001_self_receiver.rs +++ b/tests/quality_001_self_receiver.rs @@ -434,7 +434,11 @@ mod property_tests { // Must have "self" but NOT "&self" or "&mut self" let has_self = rust_code.contains(&format!("fn {method_name}(self)")) || rust_code.contains(&format!("fn {method_name} (self)")) - || rust_code.contains(&format!("fn {method_name} ( self )")); + || rust_code.contains(&format!("fn {method_name} ( self )")) + // A Rust reserved word is emitted as a raw identifier (RAWIDENT-1). + || rust_code.contains(&format!("fn r#{method_name}(self)")) + || rust_code.contains(&format!("fn r#{method_name} (self)")) + || rust_code.contains(&format!("fn r#{method_name} ( self )")); prop_assert!( has_self, @@ -564,7 +568,11 @@ mod property_tests { // For owned self, check the specific method name let has_owned_self = rust_code.contains(&format!("fn {consumer}(self)")) || rust_code.contains(&format!("fn {consumer} (self)")) - || rust_code.contains(&format!("fn {consumer} ( self )")); + || rust_code.contains(&format!("fn {consumer} ( self )")) + // A Rust reserved word is emitted as a raw identifier (RAWIDENT-1). + || rust_code.contains(&format!("fn r#{consumer}(self)")) + || rust_code.contains(&format!("fn r#{consumer} (self)")) + || rust_code.contains(&format!("fn r#{consumer} ( self )")); prop_assert!( has_owned_self, diff --git a/tests/rawident_1_rust_keywords.rs b/tests/rawident_1_rust_keywords.rs new file mode 100644 index 000000000..0ad72e98f --- /dev/null +++ b/tests/rawident_1_rust_keywords.rs @@ -0,0 +1,141 @@ +//! RAWIDENT-1: a Ruchy identifier that is a Rust reserved word but not a +//! Ruchy keyword (`do`, `box`, `typeof`, ...) used as a function or method +//! NAME must be emitted as a raw identifier (`r#do`) at both the definition +//! and the call site, so rustc accepts the transpiled code. Before the fix, +//! `Transpiler::safe_ident` did not exist and `fn do(&self)` / `obj.do()` +//! were emitted verbatim, which rustc rejects (E0379/E0533-class syntax +//! errors: `do` etc. are Rust keywords even though Ruchy allows them as +//! plain identifiers). +//! +//! This test transpiles a class with methods named `do` and `box` plus a +//! free function named `typeof`, compiles the result with rustc, runs the +//! binary, and checks its stdout matches `ruchy run` on the same source. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +/// A struct with two reserved-but-not-Ruchy-keyword method names, plus a +/// free function with a reserved-but-not-Ruchy-keyword name, called from +/// `main`. +const RAWIDENT_SOURCE: &str = r"class Thing { + n: i32 + new(n: i32) { self.n = n } + fun do(&self) -> i32 { self.n } + fun box(&self) -> i32 { self.n * 2 } +} +fun typeof(x: i32) -> i32 { x + 100 } +fun main() { + let t = Thing::new(5) + println(t.do()) + println(t.box()) + println(typeof(t.n)) +} +"; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(30)); + cmd +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +/// Test 1: `ruchy run` (interpreter) already prints the expected values; +/// this is the oracle the transpiled binary must match. +#[test] +fn test_rawident_1_01_interpreter_runs_reserved_word_names() { + let dir = tempfile::tempdir().expect("temp dir"); + let file = write_source(dir.path(), RAWIDENT_SOURCE); + let mut cmd = ruchy(); + cmd.arg("run").arg(&file); + assert_eq!(stdout_of(cmd, "ruchy run"), "5\n10\n105\n"); +} + +/// Test 2: the transpiler must emit raw identifiers (`r#do`, `r#box`, +/// `r#typeof`) at both the definition and the call site. +#[test] +fn test_rawident_1_02_transpile_emits_raw_identifiers() { + let dir = tempfile::tempdir().expect("temp dir"); + let file = write_source(dir.path(), RAWIDENT_SOURCE); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(&file); + let rust = stdout_of(cmd, "ruchy transpile"); + + // Definition sites + assert!( + rust.contains("fn r#do"), + "method definition `do` must be raw:\n{rust}" + ); + assert!( + rust.contains("fn r#box"), + "method definition `box` must be raw:\n{rust}" + ); + assert!( + rust.contains("fn r#typeof"), + "function definition `typeof` must be raw:\n{rust}" + ); + + // Call sites + assert!( + rust.contains(".r#do()"), + "method call `.do()` must be raw:\n{rust}" + ); + assert!( + rust.contains(".r#box()"), + "method call `.box()` must be raw:\n{rust}" + ); + assert!( + rust.contains("r#typeof(t.n)") || rust.contains("r#typeof (t . n)"), + "function call `typeof(..)` must be raw:\n{rust}" + ); +} + +/// Test 3: the transpiled program compiles with rustc and, once run, prints +/// exactly what `ruchy run` prints for the same source. +#[test] +fn test_rawident_1_03_transpiled_binary_matches_interpreter() { + let dir = tempfile::tempdir().expect("temp dir"); + let file = write_source(dir.path(), RAWIDENT_SOURCE); + + let mut transpile = ruchy(); + transpile.arg("transpile").arg(&file); + let rust = stdout_of(transpile, "ruchy transpile"); + + let main_rs = dir.path().join("main.rs"); + let bin = dir.path().join("rawident_bin"); + std::fs::write(&main_rs, &rust).expect("write main.rs"); + + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + + let mut run_bin = Command::new(&bin); + run_bin.timeout(Duration::from_secs(30)); + let binary_stdout = stdout_of(run_bin, "transpiled binary"); + + let mut interpreter = ruchy(); + interpreter.arg("run").arg(&file); + let interpreter_stdout = stdout_of(interpreter, "ruchy run"); + + assert_eq!(binary_stdout, interpreter_stdout); + assert_eq!(binary_stdout, "5\n10\n105\n"); +} From 131e96da445d516583717b46bd10c9047a9b3ac2 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 03:39:14 +0200 Subject: [PATCH 034/106] COMPILERACE-1: four transpiler test files pass --out-dir, so no .rlib lands in the crate root Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/transpiler_001_integer_arithmetic.rs | 15 +++++++++++++++ tests/transpiler_006_time_micros.rs | 12 ++++++++++++ tests/transpiler_008_pub_visibility.rs | 6 ++++++ tests/transpiler_statements_comprehensive.rs | 15 +++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/tests/transpiler_001_integer_arithmetic.rs b/tests/transpiler_001_integer_arithmetic.rs index fa46ab160..dc9601efc 100644 --- a/tests/transpiler_001_integer_arithmetic.rs +++ b/tests/transpiler_001_integer_arithmetic.rs @@ -51,6 +51,9 @@ pub fn add(a: i32, b: i32) -> i32 { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_001_01_output.rs"]) .output() .expect("Failed to run rustc"); @@ -168,6 +171,9 @@ pub fn calculate(a: i32, b: i32, c: i32) -> i32 { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_001_05_output.rs"]) .output() .expect("Failed to run rustc"); @@ -218,6 +224,9 @@ impl Counter { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_001_06_output.rs"]) .output() .expect("Failed to run rustc"); @@ -251,6 +260,9 @@ pub fn concat(a: String, b: String) -> String { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_001_07_output.rs"]) .output() .expect("Failed to run rustc"); @@ -289,6 +301,9 @@ pub fn increment_by(mut value: i32, amount: i32) -> i32 { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_001_08_output.rs"]) .output() .expect("Failed to run rustc"); diff --git a/tests/transpiler_006_time_micros.rs b/tests/transpiler_006_time_micros.rs index 13bbaa433..461eb4189 100644 --- a/tests/transpiler_006_time_micros.rs +++ b/tests/transpiler_006_time_micros.rs @@ -42,6 +42,9 @@ pub fn get_time() -> u64 { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_006_01_output.rs"]) .output() .expect("Failed to run rustc"); @@ -81,6 +84,9 @@ pub fn benchmark() -> u64 { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_006_02_output.rs"]) .output() .expect("Failed to run rustc"); @@ -128,6 +134,9 @@ fn fib(n: i32) -> i32 { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_006_03_output.rs"]) .output() .expect("Failed to run rustc"); @@ -172,6 +181,9 @@ pub fn multi_benchmark() -> (u64, u64) { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_006_04_output.rs"]) .output() .expect("Failed to run rustc"); diff --git a/tests/transpiler_008_pub_visibility.rs b/tests/transpiler_008_pub_visibility.rs index c02e6dd7e..8faa8ba8a 100644 --- a/tests/transpiler_008_pub_visibility.rs +++ b/tests/transpiler_008_pub_visibility.rs @@ -62,6 +62,9 @@ impl Library { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_008_01_output.rs"]) .output() .expect("Failed to run rustc"); @@ -205,6 +208,9 @@ impl Calculator { .expect("Failed to write test file"); let rustc_result = std::process::Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args(["--crate-type", "lib", "/tmp/transpiler_008_04_output.rs"]) .output() .expect("Failed to run rustc"); diff --git a/tests/transpiler_statements_comprehensive.rs b/tests/transpiler_statements_comprehensive.rs index 8282e0c40..1d2d78d01 100644 --- a/tests/transpiler_statements_comprehensive.rs +++ b/tests/transpiler_statements_comprehensive.rs @@ -55,6 +55,9 @@ pub fn test() { .expect("Failed to write test file"); let rustc_result = Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args([ "--crate-type", "lib", @@ -171,6 +174,9 @@ pub fn test(x: i32) -> String { .expect("Failed to write test file"); let rustc_result = Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args([ "--crate-type", "lib", @@ -292,6 +298,9 @@ pub fn test(x: i32) -> String { .expect("Failed to write test file"); let rustc_result = Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args([ "--crate-type", "lib", @@ -360,6 +369,9 @@ pub fn test() { .expect("Failed to write test file"); let rustc_result = Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args([ "--crate-type", "lib", @@ -705,6 +717,9 @@ pub fn calculate(x: i32) -> i32 { .expect("Failed to write test file"); let rustc_result = Command::new("rustc") + // COMPILERACE-1: the .rlib goes to the temp dir, never the crate root. + .arg("--out-dir") + .arg(std::env::temp_dir()) .args([ "--crate-type", "lib", From 9d54776a825504151f46bcb3a836ee2477cbaea2 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 03:39:35 +0200 Subject: [PATCH 035/106] RHLGA-1: CHANGELOG for the review fixes; DEBUGORDER-1, SPAWNKW-1, POSTINC-1 filed for v5.1.0 Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 17 ++++++++++-- docs/roadmaps/roadmap.yaml | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f21354184..1c5fc72de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,15 @@ Behaviour changes a program can observe are marked **(behaviour)**. - **(behaviour)** Method receivers follow the spec (§7.11): a bare `self` is owned, and becomes `&mut self` when the method mutates it. Write `&self` for a borrowing getter. This reverses the 2026-01 change that always borrowed. + Mutation is detected through nested fields, indexes, `++`/`--`, mutating std + methods on a field (`push`, `insert`, …) and calls to other mutating + methods of the same impl. A trait default method taking an owned `self` gets + `where Self: Sized`, so it compiles. +- A function or method named with a Rust reserved word (`do`, `box`, `typeof`, …) + is emitted as a raw identifier (`r#do`) at definition and call. +- `ruchy fmt` keeps `::` paths. `i32::MAX` transpiles as a path. A user-defined + `range` function is no longer mistaken for the builtin. An awaiting `main` + with a declared return type (`-> Result<(), String>`) compiles. - `module::function()` calls keep `::` in transpiled Rust, and the parser now records the difference. A closure stored in a field can be called as `(obj.f)(x)`: field accesses carry their real source span, so the call parses. @@ -23,8 +32,11 @@ Behaviour changes a program can observe are marked **(behaviour)**. - A function that returns a `let`-bound `[]` infers `-> Vec<…>`. - `range(a, b).count()` (and `sum`/`min`/`max`) compile. A `main` that awaits runs on a small in-program executor, with no tokio needed. -- **(behaviour)** The interpreter's `{:?}` prints what Rust prints (`[1, 2]`, - not `Array([Integer(1), …])`). `println("{:?}", x)` and `format!` substitute +- **(behaviour)** The interpreter's `{:?}` prints what Rust prints for + numbers, strings, arrays, tuples, `Option` and `Result` (`[1, 2]`, not + `Array([Integer(1), …])`). It differs in two cases: struct fields print sorted + by name rather than in declaration order (DEBUGORDER-1), and chars print as + strings. `println("{:?}", x)` and `format!` substitute like the macro. `format!` no longer quotes its string arguments. - **(behaviour)** `compute_hash` returns MD5 (32 hex characters) again, as its contract says, using an in-tree RFC 1321 implementation. In 5.0.0-beta.2 a @@ -36,6 +48,7 @@ Behaviour changes a program can observe are marked **(behaviour)**. warning, when the host has no inotify instance left. - Tests: - they spawn the cargo-built binary; + - they write compiled output (`a.out`, `.rlib`) into temp dirs, never the repository; - `ruchy serve` tests stop through a shutdown seam instead of serving forever; - `release_hygiene` names a dirty tree; - pmat-written files are gitignored. diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 4b003d0c6..c71a0e396 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -3982,3 +3982,60 @@ roadmap: - tests - filed-by:RHLGA-1 notes: null +- id: DEBUGORDER-1 + github_issue: null + item_type: task + title: 'DEBUGORDER-1: interpreter {:?} prints struct fields sorted by name; rustc prints declaration order (Value keeps no field order)' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T01:39:25Z + updated: 2026-09-24T01:39:25Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - filed-by:RHLGA-1 + - release:v5.1.0 + notes: null +- id: SPAWNKW-1 + github_issue: null + item_type: task + title: 'SPAWNKW-1: a trait method named spawn does not parse because spawn is a lexer keyword (SPECIFICATION 7.1 uses fun spawn(self))' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T01:39:25Z + updated: 2026-09-24T01:39:25Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - filed-by:RHLGA-1 + - release:v5.1.0 + notes: null +- id: POSTINC-1 + github_issue: null + item_type: task + title: 'POSTINC-1: self.count++ as the tail of a unit function emits a value (E0308)' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T01:39:26Z + updated: 2026-09-24T01:39:26Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - filed-by:RHLGA-1 + - release:v5.1.0 + notes: null From 38e354844b9e5c83f475f6fbe61fa689188aa422 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:03:23 +0200 Subject: [PATCH 036/106] RAWIDENT-1: a :: path call escapes reserved segments; G2BFA2: mutation is found inside assignment targets, field and index accesses Round-2 review, lane 1 (FAIL): both findings confirmed and fixed with RED-then-GREEN tests (index-nested self mutation; m::do() through rustc). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/call_transpilation.rs | 2 +- src/backend/transpiler/mutation_detection.rs | 25 +++++++++++++++-- tests/rawident_1_rust_keywords.rs | 29 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index 172b6087b..c4d9d5fb1 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -44,7 +44,7 @@ impl Transpiler { return None; } let segments = Self::callee_path_segments(func)?; - let idents = segments.iter().map(|s| format_ident!("{}", s)); + let idents = segments.iter().map(|s| Self::safe_ident(s)); Some(quote! { #(#idents)::* }) } diff --git a/src/backend/transpiler/mutation_detection.rs b/src/backend/transpiler/mutation_detection.rs index bcbb9a0db..f787256b3 100644 --- a/src/backend/transpiler/mutation_detection.rs +++ b/src/backend/transpiler/mutation_detection.rs @@ -152,8 +152,13 @@ fn sub_expressions(expr: &Expr) -> Vec<&Expr> { | ExprKind::Unary { operand: body, .. } | ExprKind::Try { expr: body } | ExprKind::Await { expr: body } - | ExprKind::Assign { value: body, .. } - | ExprKind::CompoundAssign { value: body, .. } => vec![&**body], + | ExprKind::FieldAccess { object: body, .. } => vec![&**body], + // An assignment evaluates its target place as well as its value, so a + // mutation nested in either is found (e.g. `v[{ self.n += 1; 0 }] = 2`). + ExprKind::Assign { target, value } | ExprKind::CompoundAssign { target, value, .. } => { + vec![&**target, &**value] + } + ExprKind::IndexAccess { object, index } => vec![&**object, &**index], ExprKind::Return { value } => with_optional(Vec::new(), value.as_deref()), ExprKind::Call { func, args } => std::iter::once(&**func).chain(args).collect(), ExprKind::MethodCall { receiver, args, .. } => { @@ -819,6 +824,22 @@ mod tests { assert!(is_variable_mutated("x", &nested_compound)); } + #[test] + fn test_self_mutation_nested_in_an_index_target() { + // v[{ self.n += 1 }] = 2 — the index evaluates a mutation of `self`. + let self_n = make_expr(ExprKind::FieldAccess { + object: Box::new(ident("self")), + field: "n".to_string(), + }); + let bump = compound_assign(self_n, int_lit(1)); + let place = make_expr(ExprKind::IndexAccess { + object: Box::new(ident("v")), + index: Box::new(bump), + }); + let stmt = assign(place, int_lit(2)); + assert!(is_variable_mutated("self", &stmt)); + } + #[test] fn test_assign_nested_value() { let nested_assign = assign(ident("y"), assign(ident("x"), int_lit(1))); diff --git a/tests/rawident_1_rust_keywords.rs b/tests/rawident_1_rust_keywords.rs index 0ad72e98f..a2bfe8504 100644 --- a/tests/rawident_1_rust_keywords.rs +++ b/tests/rawident_1_rust_keywords.rs @@ -139,3 +139,32 @@ fn test_rawident_1_03_transpiled_binary_matches_interpreter() { assert_eq!(binary_stdout, interpreter_stdout); assert_eq!(binary_stdout, "5\n10\n105\n"); } + +/// Test 4: a reserved word reached through a `::` path (`m::r#do()`) is raw too, +/// so the module path call compiles (review round 2 of G2 part 2). +#[test] +fn test_rawident_1_04_module_path_call_is_raw() { + let dir = tempfile::tempdir().expect("temp dir"); + let file = write_source( + dir.path(), + "mod m {\n pub fun do() -> i32 {\n 7\n }\n}\n\nfun main() {\n println(m::do())\n}\n", + ); + let mut transpile = ruchy(); + transpile.arg("transpile").arg(&file); + let rust = stdout_of(transpile, "ruchy transpile"); + assert!(rust.contains("r#do"), "expected r#do in:\n{rust}"); + + let main_rs = dir.path().join("main.rs"); + let bin = dir.path().join("rawident_path_bin"); + std::fs::write(&main_rs, &rust).expect("write main.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + let mut run_bin = Command::new(&bin); + run_bin.timeout(Duration::from_secs(30)); + assert_eq!(stdout_of(run_bin, "transpiled binary"), "7\n"); +} From 0383bc721c610d7b1ff1b0a60ae7c518a2d6701a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:18:16 +0200 Subject: [PATCH 037/106] G2BFA2: mutation detection walks every expression kind that holds sub-expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-3 review asked whether sub_expressions skipped other kinds; it skipped 17 (Some/Ok/Err, casts, ternaries, struct/object literals, ranges, sends, string interpolation, …). They are covered in two small helpers, with a RED-then-GREEN test over five of them. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/mutation_detection.rs | 99 ++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/backend/transpiler/mutation_detection.rs b/src/backend/transpiler/mutation_detection.rs index f787256b3..3cd3b05b0 100644 --- a/src/backend/transpiler/mutation_detection.rs +++ b/src/backend/transpiler/mutation_detection.rs @@ -164,6 +164,63 @@ fn sub_expressions(expr: &Expr) -> Vec<&Expr> { ExprKind::MethodCall { receiver, args, .. } => { std::iter::once(&**receiver).chain(args).collect() } + _ => value_sub_expressions(expr), + } +} + +/// Sub-expressions of the value-building forms (constructors, casts, literals +/// with fields, ranges), so a mutation nested inside one is not missed. +fn value_sub_expressions(expr: &Expr) -> Vec<&Expr> { + match &expr.kind { + ExprKind::Throw { expr: e } + | ExprKind::Ok { value: e } + | ExprKind::Err { error: e } + | ExprKind::Some { value: e } + | ExprKind::TypeCast { expr: e, .. } + | ExprKind::Spawn { actor: e } + | ExprKind::OptionalFieldAccess { object: e, .. } => vec![&**e], + ExprKind::Ternary { + condition, + true_expr, + false_expr, + } => { + vec![&**condition, &**true_expr, &**false_expr] + } + ExprKind::Send { actor, message } => vec![&**actor, &**message], + ExprKind::ArrayInit { value, size } => vec![&**value, &**size], + ExprKind::Range { start, end, .. } => vec![&**start, &**end], + ExprKind::Break { value, .. } => value.as_deref().into_iter().collect(), + ExprKind::Set(items) => items.iter().collect(), + ExprKind::OptionalMethodCall { receiver, args, .. } => { + std::iter::once(&**receiver).chain(args).collect() + } + _ => literal_sub_expressions(expr), + } +} + +/// Sub-expressions of struct, object and interpolated-string literals. +fn literal_sub_expressions(expr: &Expr) -> Vec<&Expr> { + match &expr.kind { + ExprKind::StructLiteral { fields, base, .. } => fields + .iter() + .map(|(_, e)| e) + .chain(base.as_deref()) + .collect(), + ExprKind::ObjectLiteral { fields } => fields + .iter() + .map(|f| match f { + crate::frontend::ast::ObjectField::KeyValue { value, .. } => value, + crate::frontend::ast::ObjectField::Spread { expr } => expr, + }) + .collect(), + ExprKind::StringInterpolation { parts } => parts + .iter() + .filter_map(|p| match p { + crate::frontend::ast::StringPart::Text(_) => None, + crate::frontend::ast::StringPart::Expr(e) + | crate::frontend::ast::StringPart::ExprWithFormat { expr: e, .. } => Some(&**e), + }) + .collect(), _ => Vec::new(), } } @@ -840,6 +897,48 @@ mod tests { assert!(is_variable_mutated("self", &stmt)); } + #[test] + fn test_self_mutation_nested_in_value_forms() { + let bump = || { + let self_n = make_expr(ExprKind::FieldAccess { + object: Box::new(ident("self")), + field: "n".to_string(), + }); + compound_assign(self_n, int_lit(1)) + }; + let forms = vec![ + make_expr(ExprKind::Some { + value: Box::new(bump()), + }), + make_expr(ExprKind::TypeCast { + expr: Box::new(bump()), + target_type: "i64".to_string(), + }), + make_expr(ExprKind::Ternary { + condition: Box::new(int_lit(1)), + true_expr: Box::new(bump()), + false_expr: Box::new(int_lit(0)), + }), + make_expr(ExprKind::Range { + start: Box::new(bump()), + end: Box::new(int_lit(3)), + inclusive: false, + }), + make_expr(ExprKind::StructLiteral { + name: "P".to_string(), + fields: vec![("x".to_string(), bump())], + base: None, + }), + ]; + for form in &forms { + assert!( + is_variable_mutated("self", form), + "missed in {:?}", + form.kind + ); + } + } + #[test] fn test_assign_nested_value() { let nested_assign = assign(ident("y"), assign(ident("x"), int_lit(1))); From 0caec2d473bef590bcbcc77338b54968db60ba0a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:28:52 +0200 Subject: [PATCH 038/106] G2BFA2: mutation detection walks macro invocations and every remaining kind with sub-expressions - println!/format!/vec!/assert! parse as MacroInvocation; a self mutation in their args now gives &mut self (was E0596) - also walked: VecRepeat, TryCatch (try, catch bodies, finally), let/let-pattern else blocks, match guards, AsyncBlock, AsyncLambda, Lazy, Slice, Spread, Pipeline, list/set/dict comprehensions, Ask/ActorSend/ActorQuery, Handle, Yield, Signal, DataFrameOperation, DataFrame columns, InfraBlock and ++/-- targets - walker split into category helpers (cyclomatic 2-4 each); doc comment lists the item kinds deliberately not walked - table test: one case per walked kind; tests/g2b_walkall.rs compiles with rustc and compares with ruchy run Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/mutation_detection.rs | 777 +++++++++++++++++-- tests/g2b_walkall.rs | 139 ++++ 2 files changed, 865 insertions(+), 51 deletions(-) create mode 100644 tests/g2b_walkall.rs diff --git a/src/backend/transpiler/mutation_detection.rs b/src/backend/transpiler/mutation_detection.rs index 3cd3b05b0..1142d7d29 100644 --- a/src/backend/transpiler/mutation_detection.rs +++ b/src/backend/transpiler/mutation_detection.rs @@ -113,12 +113,35 @@ fn any_expr(expr: &Expr, pred: &dyn Fn(&Expr) -> bool) -> bool { } /// The direct sub-expressions searched for mutations. +/// +/// Every expression kind that holds a sub-expression evaluated as part of the +/// enclosing body is walked. The kinds are split across small category +/// helpers chained through their `_ =>` fallbacks: this function (sequences, +/// control flow, single-child wrappers), [`binding_sub_expressions`] (let, +/// try/catch, effect handlers, pipelines, two-operand forms), +/// [`call_sub_expressions`] (calls, actor asks, slices, data frame +/// operations) and [`literal_sub_expressions`] (struct, object, string, +/// comprehension and data frame literals). +/// +/// Deliberately NOT walked: +/// - the item definitions `Module`, `Export`, `ExportDefault`, `Struct`, +/// `TupleStruct`, `Class`, `Enum`, `Trait`, `Impl`, `Actor`, `Effect` and +/// `Extension`: they are top-level items whose bodies are other methods +/// with their own receivers, so a `self` write inside one is not a write to +/// the enclosing method's `self`; +/// - the leaves `Literal`, `Identifier`, `QualifiedName`, `None`, +/// `Continue`, `Command`, `ModuleDeclaration`, `Import`, `ImportAll`, +/// `ImportDefault`, `ExportList`, `ReExport` and `TypeAlias`, which hold no +/// sub-expression. fn sub_expressions(expr: &Expr) -> Vec<&Expr> { match &expr.kind { - ExprKind::Block(exprs) | ExprKind::List(exprs) | ExprKind::Tuple(exprs) => { - exprs.iter().collect() - } - ExprKind::Macro { args, .. } => args.iter().collect(), + ExprKind::Block(exprs) + | ExprKind::List(exprs) + | ExprKind::Tuple(exprs) + | ExprKind::Set(exprs) + | ExprKind::InfraBlock { body: exprs } + | ExprKind::Macro { args: exprs, .. } + | ExprKind::MacroInvocation { args: exprs, .. } => exprs.iter().collect(), ExprKind::If { condition, then_branch, @@ -130,75 +153,173 @@ fn sub_expressions(expr: &Expr) -> Vec<&Expr> { else_branch, .. } => with_optional(vec![expr, then_branch], else_branch.as_deref()), - ExprKind::While { - condition, body, .. - } => vec![&**condition, &**body], - ExprKind::WhileLet { expr, body, .. } - | ExprKind::For { - iter: expr, body, .. - } => { - vec![&**expr, &**body] - } ExprKind::Match { expr, arms } => std::iter::once(&**expr) - .chain(arms.iter().map(|arm| &*arm.body)) + .chain( + arms.iter() + .flat_map(|arm| arm.guard.as_deref().into_iter().chain([&*arm.body])), + ) .collect(), - ExprKind::Let { value, body, .. } | ExprKind::LetPattern { value, body, .. } => { - vec![&**value, &**body] - } - ExprKind::Binary { left, right, .. } => vec![&**left, &**right], - ExprKind::Loop { body, .. } - | ExprKind::Function { body, .. } - | ExprKind::Lambda { body, .. } - | ExprKind::Unary { operand: body, .. } - | ExprKind::Try { expr: body } - | ExprKind::Await { expr: body } - | ExprKind::FieldAccess { object: body, .. } => vec![&**body], - // An assignment evaluates its target place as well as its value, so a - // mutation nested in either is found (e.g. `v[{ self.n += 1; 0 }] = 2`). - ExprKind::Assign { target, value } | ExprKind::CompoundAssign { target, value, .. } => { - vec![&**target, &**value] - } - ExprKind::IndexAccess { object, index } => vec![&**object, &**index], - ExprKind::Return { value } => with_optional(Vec::new(), value.as_deref()), - ExprKind::Call { func, args } => std::iter::once(&**func).chain(args).collect(), - ExprKind::MethodCall { receiver, args, .. } => { - std::iter::once(&**receiver).chain(args).collect() + ExprKind::Return { value } | ExprKind::Break { value, .. } | ExprKind::Yield { value } => { + value.as_deref().into_iter().collect() } - _ => value_sub_expressions(expr), + _ => single_sub_expression(expr) + .map(|e| vec![e]) + .unwrap_or_else(|| binding_sub_expressions(expr)), } } -/// Sub-expressions of the value-building forms (constructors, casts, literals -/// with fields, ranges), so a mutation nested inside one is not missed. -fn value_sub_expressions(expr: &Expr) -> Vec<&Expr> { +/// The one sub-expression of a single-child form (wrappers, closures, `++`/`--`). +fn single_sub_expression(expr: &Expr) -> Option<&Expr> { match &expr.kind { - ExprKind::Throw { expr: e } + ExprKind::Loop { body: e, .. } + | ExprKind::Function { body: e, .. } + | ExprKind::Lambda { body: e, .. } + | ExprKind::AsyncLambda { body: e, .. } + | ExprKind::AsyncBlock { body: e } + | ExprKind::Lazy { expr: e } + | ExprKind::Unary { operand: e, .. } + | ExprKind::Try { expr: e } + | ExprKind::Await { expr: e } + | ExprKind::FieldAccess { object: e, .. } + | ExprKind::OptionalFieldAccess { object: e, .. } + | ExprKind::Throw { expr: e } | ExprKind::Ok { value: e } | ExprKind::Err { error: e } | ExprKind::Some { value: e } | ExprKind::TypeCast { expr: e, .. } | ExprKind::Spawn { actor: e } - | ExprKind::OptionalFieldAccess { object: e, .. } => vec![&**e], + | ExprKind::Spread { expr: e } + | ExprKind::Signal { initial_value: e } => Some(&**e), + // The target place of `++`/`--` is evaluated too (`v[{ self.n += 1; 0 }]++`). + ExprKind::PreIncrement { target: e } + | ExprKind::PostIncrement { target: e } + | ExprKind::PreDecrement { target: e } + | ExprKind::PostDecrement { target: e } => Some(&**e), + _ => None, + } +} + +/// Sub-expressions of the binding, handler and two-operand forms. +fn binding_sub_expressions(expr: &Expr) -> Vec<&Expr> { + match &expr.kind { + ExprKind::Let { + value, + body, + else_block, + .. + } + | ExprKind::LetPattern { + value, + body, + else_block, + .. + } => with_optional(vec![value, body], else_block.as_deref()), + ExprKind::TryCatch { + try_block, + catch_clauses, + finally_block, + } => std::iter::once(&**try_block) + .chain(catch_clauses.iter().map(|c| &*c.body)) + .chain(finally_block.as_deref()) + .collect(), + ExprKind::Handle { expr, handlers } => std::iter::once(&**expr) + .chain(handlers.iter().map(|h| &*h.body)) + .collect(), + ExprKind::Pipeline { expr, stages } => std::iter::once(&**expr) + .chain(stages.iter().map(|s| &*s.op)) + .collect(), ExprKind::Ternary { condition, true_expr, false_expr, - } => { - vec![&**condition, &**true_expr, &**false_expr] + } => vec![&**condition, &**true_expr, &**false_expr], + // An assignment evaluates its target place as well as its value, so a + // mutation nested in either is found (e.g. `v[{ self.n += 1; 0 }] = 2`). + ExprKind::Assign { + target: a, + value: b, } - ExprKind::Send { actor, message } => vec![&**actor, &**message], - ExprKind::ArrayInit { value, size } => vec![&**value, &**size], - ExprKind::Range { start, end, .. } => vec![&**start, &**end], - ExprKind::Break { value, .. } => value.as_deref().into_iter().collect(), - ExprKind::Set(items) => items.iter().collect(), - ExprKind::OptionalMethodCall { receiver, args, .. } => { - std::iter::once(&**receiver).chain(args).collect() + | ExprKind::CompoundAssign { + target: a, + value: b, + .. } + | ExprKind::Binary { + left: a, right: b, .. + } + | ExprKind::IndexAccess { + object: a, + index: b, + } + | ExprKind::While { + condition: a, + body: b, + .. + } + | ExprKind::WhileLet { + expr: a, body: b, .. + } + | ExprKind::For { + iter: a, body: b, .. + } + | ExprKind::Range { + start: a, end: b, .. + } + | ExprKind::ArrayInit { value: a, size: b } + | ExprKind::VecRepeat { value: a, count: b } + | ExprKind::Send { + actor: a, + message: b, + } + | ExprKind::ActorSend { + actor: a, + message: b, + } + | ExprKind::ActorQuery { + actor: a, + message: b, + } => vec![&**a, &**b], + _ => call_sub_expressions(expr), + } +} + +/// Sub-expressions of calls, actor asks, slices and data frame operations. +fn call_sub_expressions(expr: &Expr) -> Vec<&Expr> { + match &expr.kind { + ExprKind::Call { func: r, args } + | ExprKind::MethodCall { + receiver: r, args, .. + } + | ExprKind::OptionalMethodCall { + receiver: r, args, .. + } => std::iter::once(&**r).chain(args).collect(), + ExprKind::Ask { + actor, + message, + timeout, + } => with_optional(vec![actor, message], timeout.as_deref()), + ExprKind::Slice { object, start, end } => std::iter::once(&**object) + .chain(start.as_deref()) + .chain(end.as_deref()) + .collect(), + ExprKind::DataFrameOperation { source, operation } => std::iter::once(&**source) + .chain(data_frame_op_expr(operation)) + .collect(), _ => literal_sub_expressions(expr), } } -/// Sub-expressions of struct, object and interpolated-string literals. +/// The expression argument of a data frame operation (`filter(pred)`, `join(other)`). +fn data_frame_op_expr(op: &crate::frontend::ast::DataFrameOp) -> Option<&Expr> { + use crate::frontend::ast::DataFrameOp; + match op { + DataFrameOp::Filter(e) | DataFrameOp::Join { other: e, .. } => Some(&**e), + _ => None, + } +} + +/// Sub-expressions of struct, object, interpolated-string, comprehension and +/// data frame literals. fn literal_sub_expressions(expr: &Expr) -> Vec<&Expr> { match &expr.kind { ExprKind::StructLiteral { fields, base, .. } => fields @@ -221,10 +342,32 @@ fn literal_sub_expressions(expr: &Expr) -> Vec<&Expr> { | crate::frontend::ast::StringPart::ExprWithFormat { expr: e, .. } => Some(&**e), }) .collect(), + ExprKind::ListComprehension { element, clauses } + | ExprKind::SetComprehension { element, clauses } => std::iter::once(&**element) + .chain(clause_exprs(clauses)) + .collect(), + ExprKind::DictComprehension { + key, + value, + clauses, + } => [&**key, &**value] + .into_iter() + .chain(clause_exprs(clauses)) + .collect(), + ExprKind::DataFrame { columns } => columns.iter().flat_map(|c| &c.values).collect(), _ => Vec::new(), } } +/// The iterables and filter conditions of comprehension clauses. +fn clause_exprs( + clauses: &[crate::frontend::ast::ComprehensionClause], +) -> impl Iterator { + clauses + .iter() + .flat_map(|c| std::iter::once(&*c.iterable).chain(c.condition.as_deref())) +} + /// `required` (as plain references) followed by `optional` when present. fn with_optional<'a>(required: Vec<&'a Box>, optional: Option<&'a Expr>) -> Vec<&'a Expr> { required.into_iter().map(|e| &**e).chain(optional).collect() @@ -939,6 +1082,538 @@ mod tests { } } + // ==================== Walk-every-kind table (G2BFA2) ==================== + + /// `self.n += 1`: the mutation every table case nests. + fn bump() -> Expr { + let self_n = make_expr(ExprKind::FieldAccess { + object: Box::new(ident("self")), + field: "n".to_string(), + }); + compound_assign(self_n, int_lit(1)) + } + + fn bx(e: Expr) -> Box { + Box::new(e) + } + + fn clause( + iterable: Expr, + condition: Option, + ) -> crate::frontend::ast::ComprehensionClause { + crate::frontend::ast::ComprehensionClause { + variable: "i".to_string(), + iterable: bx(iterable), + condition: condition.map(bx), + } + } + + /// One case per walked expression kind (and per walked field where a kind + /// holds several), each nesting `self.n += 1`. Deleting any walk arm fails + /// exactly the cases naming that kind. + #[allow(clippy::too_many_lines)] + fn walk_cases() -> Vec<(&'static str, Expr)> { + use crate::frontend::ast::{ + CatchClause, DataFrameColumn, DataFrameOp, EffectHandler, JoinType, ObjectField, + PipelineStage, StringPart, + }; + let e = make_expr; + let wild_arm = |guard: Option, body: Expr| MatchArm { + pattern: Pattern::Wildcard, + guard: guard.map(bx), + body: bx(body), + span: Span::default(), + }; + let let_else = |else_block: Expr| ExprKind::Let { + name: "y".to_string(), + type_annotation: None, + value: bx(int_lit(1)), + body: bx(int_lit(0)), + is_mutable: false, + else_block: Some(bx(else_block)), + }; + let try_catch = |t: Expr, c: Expr, f: Option| ExprKind::TryCatch { + try_block: bx(t), + catch_clauses: vec![CatchClause { + pattern: Pattern::Wildcard, + body: bx(c), + }], + finally_block: f.map(bx), + }; + vec![ + // ---- walked before commit 0383bc72 + ("Block", block(vec![bump()])), + ("List", e(ExprKind::List(vec![bump()]))), + ("Tuple", e(ExprKind::Tuple(vec![bump()]))), + ( + "Macro", + e(ExprKind::Macro { + name: "sql".into(), + args: vec![bump()], + }), + ), + ("If", if_expr(bool_lit(true), bump(), None)), + ( + "IfLet", + e(ExprKind::IfLet { + pattern: Pattern::Wildcard, + expr: bx(int_lit(1)), + then_branch: bx(bump()), + else_branch: None, + }), + ), + ("While", while_expr(bool_lit(true), bump())), + ( + "WhileLet", + e(ExprKind::WhileLet { + label: None, + pattern: Pattern::Wildcard, + expr: bx(int_lit(1)), + body: bx(bump()), + }), + ), + ("For", for_expr("i", ident("v"), bump())), + ( + "Match arm body", + match_expr(int_lit(1), vec![wild_arm(None, bump())]), + ), + ("Let value", let_expr("y", bump(), int_lit(0))), + ("Binary", binary(int_lit(1), bump())), + ( + "Loop", + e(ExprKind::Loop { + label: None, + body: bx(bump()), + }), + ), + ("Lambda", lambda(vec![], bump())), + ("Unary", unary(bump())), + ("Try", e(ExprKind::Try { expr: bx(bump()) })), + ("Await", e(ExprKind::Await { expr: bx(bump()) })), + ("CompoundAssign value", compound_assign(ident("z"), bump())), + ( + "Return", + e(ExprKind::Return { + value: Some(bx(bump())), + }), + ), + ("Call", call(ident("f"), vec![bump()])), + ("MethodCall", method_call(ident("o"), "m", vec![bump()])), + // ---- arms added in commit 0383bc72 + ("Some", e(ExprKind::Some { value: bx(bump()) })), + ("Ok", e(ExprKind::Ok { value: bx(bump()) })), + ("Err", e(ExprKind::Err { error: bx(bump()) })), + ("Throw", e(ExprKind::Throw { expr: bx(bump()) })), + ( + "TypeCast", + e(ExprKind::TypeCast { + expr: bx(bump()), + target_type: "i64".into(), + }), + ), + ( + "Ternary", + e(ExprKind::Ternary { + condition: bx(bool_lit(true)), + true_expr: bx(int_lit(0)), + false_expr: bx(bump()), + }), + ), + ("Spawn", e(ExprKind::Spawn { actor: bx(bump()) })), + ( + "OptionalFieldAccess", + e(ExprKind::OptionalFieldAccess { + object: bx(bump()), + field: "f".into(), + }), + ), + ( + "Send", + e(ExprKind::Send { + actor: bx(ident("a")), + message: bx(bump()), + }), + ), + ( + "ArrayInit", + e(ExprKind::ArrayInit { + value: bx(int_lit(0)), + size: bx(bump()), + }), + ), + ( + "Range", + e(ExprKind::Range { + start: bx(int_lit(0)), + end: bx(bump()), + inclusive: false, + }), + ), + ( + "Break", + e(ExprKind::Break { + label: None, + value: Some(bx(bump())), + }), + ), + ("Set", e(ExprKind::Set(vec![bump()]))), + ( + "OptionalMethodCall", + e(ExprKind::OptionalMethodCall { + receiver: bx(ident("o")), + method: "m".into(), + args: vec![bump()], + }), + ), + ( + "StructLiteral", + e(ExprKind::StructLiteral { + name: "P".into(), + fields: vec![("x".into(), bump())], + base: None, + }), + ), + ( + "ObjectLiteral", + e(ExprKind::ObjectLiteral { + fields: vec![ObjectField::KeyValue { + key: "k".into(), + value: bump(), + }], + }), + ), + ( + "StringInterpolation", + e(ExprKind::StringInterpolation { + parts: vec![StringPart::Expr(bx(bump()))], + }), + ), + ( + "FieldAccess", + e(ExprKind::FieldAccess { + object: bx(bump()), + field: "f".into(), + }), + ), + ( + "IndexAccess", + e(ExprKind::IndexAccess { + object: bx(ident("v")), + index: bx(bump()), + }), + ), + ( + "Assign target", + assign( + e(ExprKind::IndexAccess { + object: bx(ident("v")), + index: bx(bump()), + }), + int_lit(2), + ), + ), + // ---- kinds the round-3 review found unwalked + ( + "MacroInvocation", + e(ExprKind::MacroInvocation { + name: "println".into(), + args: vec![bump()], + }), + ), + ( + "VecRepeat", + e(ExprKind::VecRepeat { + value: bx(int_lit(0)), + count: bx(bump()), + }), + ), + ("TryCatch try_block", e(try_catch(bump(), int_lit(0), None))), + ( + "TryCatch catch body", + e(try_catch(int_lit(0), bump(), None)), + ), + ( + "TryCatch finally_block", + e(try_catch(int_lit(0), int_lit(0), Some(bump()))), + ), + ("Let else_block", e(let_else(bump()))), + ( + "LetPattern else_block", + e(ExprKind::LetPattern { + pattern: Pattern::Wildcard, + type_annotation: None, + value: bx(int_lit(1)), + body: bx(int_lit(0)), + is_mutable: false, + else_block: Some(bx(bump())), + }), + ), + ( + "MatchArm guard", + match_expr(int_lit(1), vec![wild_arm(Some(bump()), int_lit(0))]), + ), + ("AsyncBlock", e(ExprKind::AsyncBlock { body: bx(bump()) })), + ( + "AsyncLambda", + e(ExprKind::AsyncLambda { + params: vec![], + body: bx(bump()), + }), + ), + ("Lazy", e(ExprKind::Lazy { expr: bx(bump()) })), + ( + "Slice object", + e(ExprKind::Slice { + object: bx(bump()), + start: None, + end: None, + }), + ), + ( + "Slice start", + e(ExprKind::Slice { + object: bx(ident("v")), + start: Some(bx(bump())), + end: None, + }), + ), + ( + "Slice end", + e(ExprKind::Slice { + object: bx(ident("v")), + start: None, + end: Some(bx(bump())), + }), + ), + ("Spread", e(ExprKind::Spread { expr: bx(bump()) })), + ( + "Pipeline expr", + e(ExprKind::Pipeline { + expr: bx(bump()), + stages: vec![], + }), + ), + ( + "Pipeline stage", + e(ExprKind::Pipeline { + expr: bx(ident("v")), + stages: vec![PipelineStage { + op: bx(bump()), + span: Span::default(), + }], + }), + ), + ( + "ListComprehension element", + e(ExprKind::ListComprehension { + element: bx(bump()), + clauses: vec![clause(ident("v"), None)], + }), + ), + ( + "ListComprehension iterable", + e(ExprKind::ListComprehension { + element: bx(ident("i")), + clauses: vec![clause(bump(), None)], + }), + ), + ( + "ListComprehension condition", + e(ExprKind::ListComprehension { + element: bx(ident("i")), + clauses: vec![clause(ident("v"), Some(bump()))], + }), + ), + ( + "SetComprehension", + e(ExprKind::SetComprehension { + element: bx(bump()), + clauses: vec![clause(ident("v"), None)], + }), + ), + ( + "DictComprehension key", + e(ExprKind::DictComprehension { + key: bx(bump()), + value: bx(ident("i")), + clauses: vec![clause(ident("v"), None)], + }), + ), + ( + "DictComprehension value", + e(ExprKind::DictComprehension { + key: bx(ident("i")), + value: bx(bump()), + clauses: vec![clause(ident("v"), None)], + }), + ), + ( + "DictComprehension clause", + e(ExprKind::DictComprehension { + key: bx(ident("i")), + value: bx(ident("i")), + clauses: vec![clause(bump(), None)], + }), + ), + ( + "Ask", + e(ExprKind::Ask { + actor: bx(ident("a")), + message: bx(bump()), + timeout: None, + }), + ), + ( + "Ask timeout", + e(ExprKind::Ask { + actor: bx(ident("a")), + message: bx(ident("m")), + timeout: Some(bx(bump())), + }), + ), + ( + "ActorSend", + e(ExprKind::ActorSend { + actor: bx(ident("a")), + message: bx(bump()), + }), + ), + ( + "ActorQuery", + e(ExprKind::ActorQuery { + actor: bx(ident("a")), + message: bx(bump()), + }), + ), + ( + "Handle expr", + e(ExprKind::Handle { + expr: bx(bump()), + handlers: vec![], + }), + ), + ( + "Handle handler body", + e(ExprKind::Handle { + expr: bx(ident("x")), + handlers: vec![EffectHandler { + operation: "op".into(), + params: vec![], + body: bx(bump()), + }], + }), + ), + ( + "Yield", + e(ExprKind::Yield { + value: Some(bx(bump())), + }), + ), + ( + "Signal", + e(ExprKind::Signal { + initial_value: bx(bump()), + }), + ), + ( + "DataFrameOperation source", + e(ExprKind::DataFrameOperation { + source: bx(bump()), + operation: DataFrameOp::Head(1), + }), + ), + ( + "DataFrameOperation filter", + e(ExprKind::DataFrameOperation { + source: bx(ident("df")), + operation: DataFrameOp::Filter(bx(bump())), + }), + ), + ( + "DataFrameOperation join", + e(ExprKind::DataFrameOperation { + source: bx(ident("df")), + operation: DataFrameOp::Join { + other: bx(bump()), + on: vec![], + how: JoinType::Inner, + }, + }), + ), + ( + "DataFrame column", + e(ExprKind::DataFrame { + columns: vec![DataFrameColumn { + name: "c".into(), + values: vec![bump()], + }], + }), + ), + ("InfraBlock", e(ExprKind::InfraBlock { body: vec![bump()] })), + ( + "PreIncrement target", + pre_increment(e(ExprKind::IndexAccess { + object: bx(ident("v")), + index: bx(bump()), + })), + ), + ( + "PostIncrement target", + post_increment(e(ExprKind::IndexAccess { + object: bx(ident("v")), + index: bx(bump()), + })), + ), + ( + "PreDecrement target", + pre_decrement(e(ExprKind::IndexAccess { + object: bx(ident("v")), + index: bx(bump()), + })), + ), + ( + "PostDecrement target", + post_decrement(e(ExprKind::IndexAccess { + object: bx(ident("v")), + index: bx(bump()), + })), + ), + ] + } + + #[test] + fn test_g2bfa2_self_mutation_found_in_every_walked_kind() { + let missed: Vec<&str> = walk_cases() + .iter() + .filter(|(_, expr)| !is_variable_mutated("self", expr)) + .map(|(kind, _)| *kind) + .collect(); + assert!(missed.is_empty(), "self mutation missed inside: {missed:?}"); + } + + #[test] + fn test_g2bfa2_self_mutation_found_by_is_self_mutated_in_every_walked_kind() { + let none = HashSet::new(); + for (kind, expr) in walk_cases() { + assert!( + is_self_mutated(&expr, &none), + "is_self_mutated missed {kind}" + ); + } + } + + #[test] + fn test_g2bfa2_no_false_positive_without_a_mutation() { + let quiet = make_expr(ExprKind::MacroInvocation { + name: "println".into(), + args: vec![make_expr(ExprKind::FieldAccess { + object: Box::new(ident("self")), + field: "n".to_string(), + })], + }); + assert!(!is_variable_mutated("self", &quiet)); + } + #[test] fn test_assign_nested_value() { let nested_assign = assign(ident("y"), assign(ident("x"), int_lit(1))); diff --git a/tests/g2b_walkall.rs b/tests/g2b_walkall.rs new file mode 100644 index 000000000..2536b275a --- /dev/null +++ b/tests/g2b_walkall.rs @@ -0,0 +1,139 @@ +//! G2BFA2 / RAWIDENT-1 (RHLGA-1), review round 3 of G2 part 2. +//! +//! 1. `&mut self` inference walks every expression kind that holds +//! sub-expressions. The parser builds every generic macro call +//! (`println!`, `format!`, `vec!`, `assert!`) as `MacroInvocation`; a +//! mutation of `self` inside one must still make the receiver `&mut self`, +//! or rustc rejects the method (E0596). +//! +//! Every program is transpiled, compiled with rustc and run. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(30)); + cmd +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn transpile(file: &Path) -> String { + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, "ruchy transpile") +} + +/// Compile `rust` with rustc inside `dir` and return the binary's stdout. +fn compiled_stdout(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("main.rs"); + let bin = dir.join("g2b_walkall_bin"); + std::fs::write(&main_rs, rust).expect("write main.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + let mut run_bin = Command::new(&bin); + run_bin.timeout(Duration::from_secs(30)); + stdout_of(run_bin, "transpiled binary") +} + +/// Whitespace-free token text, so `fn pop_print (& mut self)` matches. +fn squash(rust: &str) -> String { + rust.chars().filter(|c| !c.is_whitespace()).collect() +} + +// --------------------------------------- 1. mutation inside a macro call + +const POP_PRINT: &str = r#"struct Stack { + stack: Vec +} +impl Stack { + fun pop_print(self) { + println!("{}", self.stack.pop().unwrap()) + } +} +fun main() { + let mut s = Stack { stack: vec![1, 2, 3] } + s.pop_print() + s.pop_print() + println("done") +} +"#; + +#[test] +fn test_g2bfa2_mutation_inside_macro_invocation_makes_receiver_mut() { + let dir = tempfile::tempdir().expect("temp dir"); + let rust = transpile(&write_source(dir.path(), POP_PRINT)); + assert!( + squash(&rust).contains("fnpop_print(&mutself)"), + "`self.stack.pop()` inside println! must give `&mut self`:\n{rust}" + ); +} + +#[test] +fn test_g2bfa2_mutation_inside_macro_invocation_compiles_and_runs() { + let dir = tempfile::tempdir().expect("temp dir"); + let rust = transpile(&write_source(dir.path(), POP_PRINT)); + assert_eq!(compiled_stdout(dir.path(), &rust), "3\n2\ndone\n"); +} + +/// The same shape with a counter, so the interpreter can be the oracle too: +/// `ruchy run` does not persist `self.stack.pop()` on a field (it evaluates to +/// the popped array), a runtime defect outside this transpiler fix. +const BUMP_PRINT: &str = r#"struct Counter { + n: i32 +} +impl Counter { + fun bump_print(self) { + println!("{}", { + self.n += 1 + self.n + }) + } +} +fun main() { + let mut c = Counter { n: 0 } + c.bump_print() + c.bump_print() + println("done") +} +"#; + +#[test] +fn test_g2bfa2_mutation_inside_macro_invocation_matches_run() { + let dir = tempfile::tempdir().expect("temp dir"); + let file = write_source(dir.path(), BUMP_PRINT); + let rust = transpile(&file); + assert!( + squash(&rust).contains("fnbump_print(&mutself)"), + "`self.n += 1` inside println! must give `&mut self`:\n{rust}" + ); + let binary_stdout = compiled_stdout(dir.path(), &rust); + + let mut interpreter = ruchy(); + interpreter.arg("run").arg(&file); + let interpreter_stdout = stdout_of(interpreter, "ruchy run"); + + assert_eq!(binary_stdout, "1\n2\ndone\n"); + assert_eq!(binary_stdout, interpreter_stdout); +} From 6e8bbfd412201e3cad612b1905e1968844a8cf3a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:29:29 +0200 Subject: [PATCH 039/106] RAWIDENT-1: a :: path used as a value escapes a reserved segment - transpile_field_access_node emits the path segment through Transpiler::safe_ident, so `let f = m::do` becomes `m::r#do` (was a token parse failure: expected identifier, found keyword `do`) - tests/g2b_walkall.rs: the value-path program transpiles, compiles with rustc and prints 7 Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../expressions_helpers/field_access.rs | 3 ++- tests/g2b_walkall.rs | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/backend/transpiler/expressions_helpers/field_access.rs b/src/backend/transpiler/expressions_helpers/field_access.rs index fb29dbfea..d55cbc6de 100644 --- a/src/backend/transpiler/expressions_helpers/field_access.rs +++ b/src/backend/transpiler/expressions_helpers/field_access.rs @@ -95,7 +95,8 @@ impl Transpiler { return self.transpile_field_access(object, field); } let obj_tokens = self.transpile_expr(object)?; - let field_ident = format_ident!("{}", field); + // A reserved segment used as a value (`let f = m::do`) is raw (`m::r#do`). + let field_ident = Self::safe_ident(field); Ok(quote! { #obj_tokens::#field_ident }) } diff --git a/tests/g2b_walkall.rs b/tests/g2b_walkall.rs index 2536b275a..ddc8aa834 100644 --- a/tests/g2b_walkall.rs +++ b/tests/g2b_walkall.rs @@ -5,6 +5,8 @@ //! (`println!`, `format!`, `vec!`, `assert!`) as `MacroInvocation`; a //! mutation of `self` inside one must still make the receiver `&mut self`, //! or rustc rejects the method (E0596). +//! 2. A `::` path used as a value (not called), `let f = m::do`, emits a +//! reserved segment as a raw identifier. //! //! Every program is transpiled, compiled with rustc and run. @@ -137,3 +139,28 @@ fn test_g2bfa2_mutation_inside_macro_invocation_matches_run() { assert_eq!(binary_stdout, "1\n2\ndone\n"); assert_eq!(binary_stdout, interpreter_stdout); } + +// ------------------------------------ 2. reserved `::` segment as a value + +const PATH_VALUE: &str = r"mod m { + pub fun do() -> i32 { + 7 + } +} + +fun main() { + let f = m::do + println(f()) +} +"; + +#[test] +fn test_rawident_1_path_used_as_value_is_raw_and_compiles() { + let dir = tempfile::tempdir().expect("temp dir"); + let rust = transpile(&write_source(dir.path(), PATH_VALUE)); + assert!( + squash(&rust).contains("m::r#do;"), + "`m::do` as a value must be `m::r#do`:\n{rust}" + ); + assert_eq!(compiled_stdout(dir.path(), &rust), "7\n"); +} From 55e4d024ee6bf2eec501ab0417351d6d2ed9d31f Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:31:58 +0200 Subject: [PATCH 040/106] RHLGA-1: file FIELDPOP-1 and VECLIT-1 Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index c71a0e396..d2829e7f8 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4039,3 +4039,41 @@ roadmap: - filed-by:RHLGA-1 - release:v5.1.0 notes: null +- id: FIELDPOP-1 + github_issue: null + item_type: task + title: 'FIELDPOP-1: self.items.pop() inside a method returns the whole array instead of the popped element (interpreter)' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T05:31:58Z + updated: 2026-09-24T05:31:58Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 + notes: null +- id: VECLIT-1 + github_issue: null + item_type: task + title: 'VECLIT-1: an array literal assigned to a Vec field transpiles to an array, rustc E0308' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T05:31:58Z + updated: 2026-09-24T05:31:58Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null From d5f72a35f53fd1b779cafc01a5709ecbfd115216 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:37:22 +0200 Subject: [PATCH 041/106] FIELDPOP-1: a push/pop on a field returns its own result and writes the field back self.items.pop() evaluated the field to a copy and returned the updated copy (so .unwrap() failed with 'Unknown array method: unwrap') and the field kept its items. try_field_array_mutation runs push/pop on a field rooted at an identifier and writes the array back through eval_field_assign (Struct rebinds, Object copies, ObjectMut/Class mutate in place). - tests/fieldpop_1_field_methods.rs: 10 tests incl. field-vs-local parity and a rustc differential; 3/3 hand mutants caught Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 9 +- src/runtime/interpreter_control_flow.rs | 2 +- src/runtime/interpreter_methods_dispatch.rs | 73 +++--- tests/fieldpop_1_field_methods.rs | 263 ++++++++++++++++++++ 4 files changed, 316 insertions(+), 31 deletions(-) create mode 100644 tests/fieldpop_1_field_methods.rs diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index d2829e7f8..7ee3a7bfb 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4043,13 +4043,18 @@ roadmap: github_issue: null item_type: task title: 'FIELDPOP-1: self.items.pop() inside a method returns the whole array instead of the popped element (interpreter)' - status: planned + status: inprogress priority: critical assigned_to: null created: 2026-09-24T05:31:58Z updated: 2026-09-24T05:31:58Z spec: null - acceptance_criteria: [] + acceptance_criteria: + - 'Repro: struct Stack { items: Vec } with fun pop_one(&mut self) -> i32 { self.items.pop().unwrap() }; ruchy run failed "Unknown array method: unwrap" (self.items.pop() returned the remaining array [1, 2]) and s.items.len() stayed 3; s.items.push(x) / o.items.pop() on a struct variable or object literal did not change the field either' + - 'Root cause: Interpreter::eval_method_call special-cased push/pop only for an identifier receiver and push only for an ObjectMut field; any other field receiver was evaluated to a copy and dispatched to eval_array_method, whose functional pop/push return the updated copy, and nothing wrote the copy back to the field' + - 'Fix: try_field_array_mutation (interpreter_methods_dispatch.rs) runs push/pop on a field rooted at an identifier (self, a struct/object/class variable), returns the element (nil when empty) or nil for push, and writes the array back through eval_field_assign (Struct rebinds, Object copies, ObjectMut/Class mutate in place)' + - 'tests/fieldpop_1_field_methods.rs: self/struct/object/class field push+pop, while-loop persistence, empty pop, field-vs-local parity for pop/push/sorted/reversed/drop/append/sort/reverse/len, differential through rustc; 3/3 hand mutants caught' + - 'Open: the exact repro still fails in the interpreter with "Unknown integer method: unwrap" because array pop returns the raw element (not Option) and the interpreter has no Option unwrap (Some(3).unwrap() fails: Method unwrap not found for type enum_variant)' phases: [] subtasks: [] estimated_effort: null diff --git a/src/runtime/interpreter_control_flow.rs b/src/runtime/interpreter_control_flow.rs index 04aad4bde..ebea81c20 100644 --- a/src/runtime/interpreter_control_flow.rs +++ b/src/runtime/interpreter_control_flow.rs @@ -487,7 +487,7 @@ impl Interpreter { /// Evaluate field assignment: `obj.field = value`. /// /// Handles Object, ObjectMut, Class, and Struct field updates. - fn eval_field_assign( + pub(crate) fn eval_field_assign( &mut self, object: &Expr, field: &str, diff --git a/src/runtime/interpreter_methods_dispatch.rs b/src/runtime/interpreter_methods_dispatch.rs index 5b42f7005..f25950317 100644 --- a/src/runtime/interpreter_methods_dispatch.rs +++ b/src/runtime/interpreter_methods_dispatch.rs @@ -15,7 +15,48 @@ use crate::runtime::interpreter::Interpreter; use crate::runtime::{InterpreterError, Value}; use std::sync::Arc; +/// FIELDPOP-1: array methods that change their receiver in place — the same +/// set the interpreter mutates for a local array receiver (`a.push(x)`, `a.pop()`). +fn is_mutating_array_call(method: &str, arg_count: usize) -> bool { + matches!((method, arg_count), ("push", 1) | ("pop", 0)) +} + impl Interpreter { + /// FIELDPOP-1: `obj.field.push(x)` / `obj.field.pop()` on an array field + /// behave like the same call on a local array: the call returns the + /// method's own result (`pop` → the removed element, `nil` when empty; + /// `push` → `nil`) and the changed array is written back to the field. + /// + /// Returns `Ok(None)` when the call is not of that shape, so the caller + /// falls through to ordinary method dispatch. + fn try_field_array_mutation( + &mut self, + receiver: &Expr, + method: &str, + args: &[Expr], + ) -> Result, InterpreterError> { + let ExprKind::FieldAccess { object, field } = &receiver.kind else { + return Ok(None); + }; + let rooted = matches!(object.kind, ExprKind::Identifier(_)); + if !rooted || !is_mutating_array_call(method, args.len()) { + return Ok(None); + } + let Value::Array(arr) = self.eval_expr(receiver)? else { + return Ok(None); + }; + let mut items = arr.to_vec(); + let result = match args.first() { + Some(arg) => { + items.push(self.eval_expr(arg)?); + Value::Nil + } + None => items.pop().unwrap_or(Value::Nil), + }; + self.eval_field_assign(object, field, Value::Array(Arc::from(items)))?; + Ok(Some(result)) + } + /// Evaluate a method call pub(crate) fn eval_method_call( &mut self, @@ -65,34 +106,10 @@ impl Interpreter { } } - // Special handling for mutating array methods on ObjectMut fields - // e.g., self.messages.push(item) - if let ExprKind::FieldAccess { object, field } = &receiver.kind { - if let Ok(object_value) = self.eval_expr(object) { - if let Value::ObjectMut(cell_rc) = object_value { - // Check if this is a mutating array method - if method == "push" && args.len() == 1 { - // Evaluate the argument - let arg_value = self.eval_expr(&args[0])?; - - // Get mutable access to the object - let mut obj = cell_rc - .lock() - .expect("Mutex poisoned: object lock is corrupted"); - - // Get the field value - if let Some(field_value) = obj.get(field) { - // If it's an array, push to it - if let Value::Array(arr) = field_value { - let mut new_arr = arr.to_vec(); - new_arr.push(arg_value); - obj.insert(field.clone(), Value::Array(Arc::from(new_arr))); - return Ok(Value::Nil); // push returns nil - } - } - } - } - } + // FIELDPOP-1: mutating array methods on a field (self.items.pop(), + // s.items.push(x)) return their own result and write the field back + if let Some(result) = self.try_field_array_mutation(receiver, method, args)? { + return Ok(result); } let receiver_value = self.eval_expr(receiver)?; diff --git a/tests/fieldpop_1_field_methods.rs b/tests/fieldpop_1_field_methods.rs new file mode 100644 index 000000000..181e31cdc --- /dev/null +++ b/tests/fieldpop_1_field_methods.rs @@ -0,0 +1,263 @@ +//! FIELDPOP-1: a mutating array method called on a field (`self.items.pop()`, +//! `s.items.push(x)`, `o.items.pop()`) returns the method's own result and +//! writes the changed array back to the field. It used to evaluate the field +//! to a copy, run the array method on the copy, and return the updated copy: +//! `self.items.pop()` yielded the remaining array (so `.unwrap()` failed with +//! "Unknown array method: unwrap") and the field kept all its items. +//! +//! The field form must behave exactly like the same call on a local array +//! (`let mut a = [1, 2, 3]; a.pop()` returns `3`, leaves `a == [1, 2]`), so +//! most tests compare the two forms instead of fixing one semantics here. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(30)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn interpret(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + stdout_of(cmd, "ruchy run") +} + +fn run_file(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + interpret(dir.path(), src) +} + +fn transpile(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, "ruchy transpile") +} + +fn rustc_run(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("main.rs"); + let bin = dir.join("prog_bin"); + std::fs::write(&main_rs, rust).expect("write main.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "transpiled binary") +} + +/// Transpile, compile, run; assert interpreter == rustc output. Returns the Rust text. +fn differential(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), src); + let expected = interpret(dir.path(), src); + let actual = rustc_run(dir.path(), &rust); + assert_eq!( + actual, expected, + "transpiled Rust disagrees with the interpreter\nsource:\n{src}\nrust:\n{rust}" + ); + rust +} + +const STACK: &str = "struct Stack { items: Vec }\n"; + +/// `op` applied to a local array and to a struct field must print the same +/// result and leave the same array behind. +fn assert_field_matches_local(op: &str) { + let local = format!( + "fun main() {{\n let mut a = [3, 1, 2]\n println(a.{op})\n println(a)\n}}\n" + ); + let field = format!( + "{STACK}fun main() {{\n let mut s = Stack {{ items: vec![3, 1, 2] }}\n println(s.items.{op})\n println(s.items)\n}}\n" + ); + assert_eq!( + run_file(&field), + run_file(&local), + "field vs local for `{op}`" + ); +} + +#[test] +fn test_fieldpop_1_01_self_field_pop_returns_element_and_writes_back() { + let src = format!( + "{STACK}impl Stack {{ + fun pop_one(&mut self) -> i32 {{ + self.items.pop() + }} +}} +fun main() {{ + let mut s = Stack {{ items: vec![1, 2, 3] }} + println(s.pop_one()) + println(s.items.len()) + println(s.items) +}} +" + ); + assert_eq!(run_file(&src), "3\n2\n[1, 2]\n"); +} + +#[test] +fn test_fieldpop_1_02_self_field_push_writes_back() { + let src = format!( + "{STACK}impl Stack {{ + fun push_two(&mut self, x: i32) {{ + self.items.push(x) + self.items.push(x + 1) + }} +}} +fun main() {{ + let mut s = Stack {{ items: vec![1] }} + s.push_two(10) + println(s.items) +}} +" + ); + assert_eq!(run_file(&src), "[1, 10, 11]\n"); +} + +#[test] +fn test_fieldpop_1_03_struct_value_field_pop_and_push_in_main() { + let src = format!( + "{STACK}fun main() {{ + let mut s = Stack {{ items: vec![1, 2, 3] }} + let top = s.items.pop() + println(top) + println(s.items) + s.items.push(9) + println(s.items) +}} +" + ); + assert_eq!(run_file(&src), "3\n[1, 2]\n[1, 2, 9]\n"); +} + +#[test] +fn test_fieldpop_1_04_field_push_inside_while_loop_persists() { + let src = format!( + "{STACK}fun main() {{ + let mut s = Stack {{ items: vec![] }} + let mut i = 0 + while i < 3 {{ + s.items.push(i) + i = i + 1 + }} + println(s.items) +}} +" + ); + assert_eq!(run_file(&src), "[0, 1, 2]\n"); +} + +#[test] +fn test_fieldpop_1_05_object_literal_field_pop_and_push() { + let src = "fun main() { + let mut o = {items: [1, 2, 3]} + println(o.items.pop()) + o.items.push(7) + println(o.items) +} +"; + assert_eq!(run_file(src), "3\n[1, 2, 7]\n"); +} + +#[test] +fn test_fieldpop_1_06_class_field_push_pop_reference_semantics() { + let src = "class Bag { + items: Vec + new() { + self.items = [] + } + fun add(&mut self, x: i32) { self.items.push(x) } + fun take(&mut self) -> i32 { self.items.pop() } +} +let mut b = Bag::new() +let alias = b +b.add(4) +b.add(5) +println(b.take()) +println(b.items) +println(alias.items) +"; + assert_eq!(run_file(src), "5\n[4]\n[4]\n"); +} + +#[test] +fn test_fieldpop_1_07_empty_field_pop_matches_local() { + let local = + run_file("fun main() {\n let mut a = []\n println(a.pop())\n println(a)\n}\n"); + let field = run_file(&format!( + "{STACK}fun main() {{\n let mut s = Stack {{ items: vec![] }}\n println(s.items.pop())\n println(s.items)\n}}\n" + )); + assert_eq!(field, local); +} + +#[test] +fn test_fieldpop_1_08_mutating_forms_match_local() { + for op in ["pop()", "push(4)"] { + assert_field_matches_local(op); + } +} + +#[test] +fn test_fieldpop_1_09_methods_1_and_copying_forms_match_local() { + for op in [ + "sorted()", + "reversed()", + "drop(1)", + "append([4])", + "sort()", + "reverse()", + "len()", + ] { + assert_field_matches_local(op); + } +} + +#[test] +fn test_fieldpop_1_10_differential_push_pop_through_rustc() { + let src = format!( + "{STACK}impl Stack {{ + fun push_two(&mut self, x: i32) {{ + self.items.push(x) + self.items.push(x + 1) + }} + fun drop_top(&mut self) -> usize {{ + self.items.pop(); + self.items.len() + }} +}} +fun main() {{ + let mut s = Stack {{ items: vec![1, 2, 3] }} + println(s.drop_top()) + s.push_two(10) + println(s.items.len()) + println(s.items[s.items.len() - 1]) +}} +" + ); + differential(&src); +} From dfe72f6536e463941ec060526251946e4b47a4c7 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:39:19 +0200 Subject: [PATCH 042/106] =?UTF-8?q?VECLIT-1:=20an=20array=20literal=20for?= =?UTF-8?q?=20a=20Vec<=E2=80=A6>=20struct=20field=20is=20emitted=20as=20ve?= =?UTF-8?q?c![=E2=80=A6]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stack { items: [1, 2, 3] } for a field declared Vec transpiled to a Rust array and rustc failed E0308. transpile_field_value prefixes vec! when the value is a list literal and the recorded field type is Vec/Vec<…>; [T; N] fields keep the array literal. - tests/fieldpop_1_field_methods.rs: 2 rustc differential tests; hand mutant caught Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 8 +++-- .../expressions_helpers/collections.rs | 25 ++++++++++++++- tests/fieldpop_1_field_methods.rs | 31 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 7ee3a7bfb..df5213499 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4067,13 +4067,17 @@ roadmap: github_issue: null item_type: task title: 'VECLIT-1: an array literal assigned to a Vec field transpiles to an array, rustc E0308' - status: planned + status: inprogress priority: high assigned_to: null created: 2026-09-24T05:31:58Z updated: 2026-09-24T05:31:58Z spec: null - acceptance_criteria: [] + acceptance_criteria: + - 'Repro: struct Stack { items: Vec }; let s = Stack { items: [1, 2, 3] } transpiled to Stack { items: [1, 2, 3] } and rustc failed E0308 (expected Vec, found [{integer}; 3])' + - 'Root cause: transpile_struct_literal emitted every field value with transpile_expr and never consulted the recorded field type except for String; ExprKind::List emits a Rust array literal, which does not coerce to Vec' + - 'Fix: transpile_field_value (expressions_helpers/collections.rs) prefixes vec! when the value is a list literal and the field type recorded by the struct pass is Vec or Generic { base: "Vec" }; fixed-size [T; N] fields keep the array literal' + - 'tests/fieldpop_1_field_methods.rs test_veclit_1_*: rustc differential for Vec and Vec> fields plus an [i32; 2] field; hand mutant (Generic Vec not recognised) caught' phases: [] subtasks: [] estimated_effort: null diff --git a/src/backend/transpiler/expressions_helpers/collections.rs b/src/backend/transpiler/expressions_helpers/collections.rs index a8ced4030..c0abce446 100644 --- a/src/backend/transpiler/expressions_helpers/collections.rs +++ b/src/backend/transpiler/expressions_helpers/collections.rs @@ -199,6 +199,23 @@ impl Transpiler { /// let result = transpiler.transpile_struct_literal("Point", &fields, None); /// assert!(result.is_ok()); /// ``` + /// VECLIT-1: an array literal initializing a field declared `Vec<…>` is + /// emitted as `vec![…]`; a Rust array does not coerce to `Vec` (E0308). + fn transpile_field_value( + &self, + field_type: Option<&String>, + value: &Expr, + ) -> Result { + let tokens = self.transpile_expr(value)?; + let is_list = matches!(value.kind, ExprKind::List(_)); + let is_vec_field = field_type.is_some_and(|t| is_vec_type_name(t)); + let is_array_tokens = tokens.to_string().starts_with('['); + if is_list && is_vec_field && is_array_tokens { + return Ok(quote! { vec! #tokens }); + } + Ok(tokens) + } + pub fn transpile_struct_literal( &self, name: &str, @@ -254,7 +271,7 @@ impl Transpiler { field_tokens.push(quote! { #field_ident: Some(Box::new(#inner_tokens)) }); } } else { - let value_tokens = self.transpile_expr(value)?; + let value_tokens = self.transpile_field_value(field_type, value)?; if needs_to_string { field_tokens.push(quote! { #field_ident: #value_tokens.to_string() }); } else { @@ -312,6 +329,12 @@ impl Transpiler { } } +/// VECLIT-1: the recorded type of a struct field is `Vec` or a +/// `Vec<…>` generic (stored as the `TypeKind` debug text by the struct pass). +fn is_vec_type_name(type_name: &str) -> bool { + type_name == "Vec" || type_name.starts_with("Generic { base: \"Vec\"") +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/fieldpop_1_field_methods.rs b/tests/fieldpop_1_field_methods.rs index 181e31cdc..eea6e7686 100644 --- a/tests/fieldpop_1_field_methods.rs +++ b/tests/fieldpop_1_field_methods.rs @@ -261,3 +261,34 @@ fun main() {{ ); differential(&src); } + +// ---------- VECLIT-1: an array literal for a `Vec<…>` struct field ---------- + +#[test] +fn test_veclit_1_01_array_literal_for_vec_field_compiles_as_vec() { + let src = format!( + "{STACK}fun main() {{ + let mut s = Stack {{ items: [1, 2, 3] }} + s.items.push(4) + println(s.items.len()) +}} +" + ); + let rust = differential(&src); + let squashed: String = rust.chars().filter(|c| !c.is_whitespace()).collect(); + assert!(squashed.contains("items:vec![1,2,3]"), "rust:\n{rust}"); +} + +#[test] +fn test_veclit_1_02_nested_vec_field_and_fixed_array_field_untouched() { + let src = "struct Grid { rows: Vec>, fixed: [i32; 2] } +fun main() { + let g = Grid { rows: [vec![1], vec![2, 3]], fixed: [7, 8] } + println(g.rows.len()) + println(g.fixed[1]) +} +"; + let rust = differential(src); + let squashed: String = rust.chars().filter(|c| !c.is_whitespace()).collect(); + assert!(squashed.contains("fixed:[7,8]"), "rust:\n{rust}"); +} From 2e0c8d1d590fed05042ae8db8a5dbce8164f0ef7 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:45:26 +0200 Subject: [PATCH 043/106] RHLGA-1: file OPTMETHODS-1, ARRAYMUT-1, EXTENDKW-1 Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index df5213499..d5091b15a 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4086,3 +4086,67 @@ roadmap: - transpiler - filed-by:RHLGA-1 notes: null +- id: OPTMETHODS-1 + github_issue: null + item_type: task + title: 'OPTMETHODS-1: the interpreter has no Option/Result methods (Some(3).unwrap() fails) and map.get fails' + status: inprogress + priority: critical + assigned_to: null + created: 2026-09-24T08:00:00Z + updated: 2026-09-24T08:00:00Z + spec: null + acceptance_criteria: + - 'Repro: Some(3).unwrap() -> "Method unwrap not found for type enum_variant"; Ok(2).unwrap_or(0) and None.unwrap_or(5) fail the same way; {"a": 1}.get("a") -> "Object is missing __type marker"' + - 'Ruling (orchestrator, RHLGA-1): the interpreter keeps its nil-for-absent library results (pop/first/get return the element or nil); Option methods accept both representations: Some(v)/Ok(v) and a plain non-nil value act as present, None/Err(e)/nil as absent. So v.pop().unwrap() yields the element, as the transpiled Rust does' + - 'Methods: unwrap, expect, unwrap_or, unwrap_or_else, unwrap_or_default, is_some, is_none, is_ok, is_err, map, and_then, ok, ok_or, or, filter where applicable; unwrap/expect on absent is a runtime error naming the value' + - 'Acceptance: tests/optmethods_1_option_methods.rs, every method on Some/None/Ok/Err/plain/nil, plus self.items.pop().unwrap() in a struct method returning 3 then 2, differential through rustc' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 +- id: ARRAYMUT-1 + github_issue: null + item_type: task + title: 'ARRAYMUT-1: in-place array methods do not mutate (v.sort() leaves v unsorted) and insert/remove/clear/truncate are unknown (interpreter)' + status: inprogress + priority: critical + assigned_to: null + created: 2026-09-24T08:00:00Z + updated: 2026-09-24T08:00:00Z + spec: null + acceptance_criteria: + - 'Repro: let mut v = [3,1,2]; v.sort(); println(v) prints [3, 1, 2]; v.reverse() likewise; v.insert(0, 9) -> "Unknown array method: insert"; remove, clear, truncate the same' + - 'Fix: sort, reverse, append, insert, remove, clear, truncate, dedup, extend mutate the receiver in place for a local identifier and for a field receiver (try_field_array_mutation path), returning what Rust returns (unit, or the removed element for remove); sorted/reversed stay functional' + - 'Acceptance: tests/arraymut_1_in_place.rs, each method on a local and on self.field, differential through rustc' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 +- id: EXTENDKW-1 + github_issue: null + item_type: task + title: 'EXTENDKW-1: v.extend([3]) is a parse error "Expected method name, tuple index, or await after ."' + status: inprogress + priority: critical + assigned_to: null + created: 2026-09-24T08:00:00Z + updated: 2026-09-24T08:00:00Z + spec: null + acceptance_criteria: + - 'Repro: let mut v = [1,2]; v.extend([3]) -> Syntax error: Expected method name, tuple index, or await after .' + - 'Fix: a keyword token after . in method position is accepted as a method name (extend and any other contextual keyword), without changing the keyword elsewhere' + - 'Acceptance: tests/extendkw_1_keyword_methods.rs parses and transpiles v.extend(x) and each keyword that can follow . as a method name' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 From 38556a0ca72901324cbaba750997d5fa553616e2 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:48:18 +0200 Subject: [PATCH 044/106] [RHLGA-1] OPTMETHODS-1 RED: Option/Result methods and map.get in the interpreter - 13 tests: Some/None/Ok/Err/plain/nil methods, unwrap/expect errors, Stack.pop_one via self.items.pop().unwrap(), map.get, rustc differential - 11/13 fail on the current tree (Method 'unwrap' not found for type enum_variant) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/optmethods_1_option_methods.rs | 338 +++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 tests/optmethods_1_option_methods.rs diff --git a/tests/optmethods_1_option_methods.rs b/tests/optmethods_1_option_methods.rs new file mode 100644 index 000000000..bdf0cd60e --- /dev/null +++ b/tests/optmethods_1_option_methods.rs @@ -0,0 +1,338 @@ +//! OPTMETHODS-1: `Option`/`Result` methods in the interpreter. +//! +//! `Some(3).unwrap()` failed with "Method 'unwrap' not found for type +//! enum_variant" and `{"a": 1}.get("a")` with "Object is missing __type +//! marker" (the Option methods were lost in SPRINT-65). +//! +//! The interpreter keeps nil-for-absent library results (`pop`, `first`, +//! `get` return the element or nil), so these methods accept both forms: +//! `Some(v)`/`Ok(v)` and a plain non-nil value are present; `None`, `Err(e)` +//! and nil are absent. A plain value only reaches these methods for names +//! its own type does not already handle. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(30)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn interpret(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + stdout_of(cmd, "ruchy run") +} + +fn run_file(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + interpret(dir.path(), src) +} + +/// Run a program that must fail; return its stdout + stderr. +fn run_failing(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + let out = cmd.arg("run").arg(file).output().expect("spawn ruchy"); + let text = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + assert!(!out.status.success(), "expected failure, got:\n{text}"); + text +} + +fn main_of(body: &str) -> String { + format!("fun main() {{\n{body}\n}}\n") +} + +fn lines(values: &[&str]) -> String { + values.iter().map(|v| format!("{v}\n")).collect() +} + +fn transpile(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, "ruchy transpile") +} + +fn rustc_run(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("main.rs"); + let bin = dir.join("prog_bin"); + std::fs::write(&main_rs, rust).expect("write main.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "transpiled binary") +} + +/// Transpile, compile, run; assert interpreter == rustc output. Returns the output. +fn differential(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), src); + let expected = interpret(dir.path(), src); + let actual = rustc_run(dir.path(), &rust); + assert_eq!( + actual, expected, + "transpiled Rust disagrees with the interpreter\nsource:\n{src}\nrust:\n{rust}" + ); + expected +} + +#[test] +fn test_optmethods_1_01_some_methods() { + let src = main_of( + r#" let s = Some(3) + println(s.unwrap()) + println(s.expect("need a value")) + println(s.unwrap_or(0)) + println(s.unwrap_or_else(|| 9)) + println(s.unwrap_or_default()) + println(s.is_some()) + println(s.is_none()) + println(s.map(|x| x * 2).unwrap()) + println(s.and_then(|x| Some(x + 1)).unwrap()) + println(s.or(Some(7)).unwrap()) + println(s.filter(|x| x > 1).is_some()) + println(s.filter(|x| x > 5).is_none()) + println(s.ok_or("missing").unwrap())"#, + ); + assert_eq!( + run_file(&src), + lines(&["3", "3", "3", "3", "3", "true", "false", "6", "4", "3", "true", "true", "3"]) + ); +} + +#[test] +fn test_optmethods_1_02_none_methods() { + let src = main_of( + r#" let n = None + println(n.unwrap_or(5)) + println(n.unwrap_or_else(|| 9)) + println(n.is_some()) + println(n.is_none()) + println(n.map(|x| x * 2).is_none()) + println(n.and_then(|x| Some(x)).is_none()) + println(n.or(Some(7)).unwrap()) + println(n.filter(|x| x > 1).is_none()) + println(n.ok_or("missing").is_err())"#, + ); + assert_eq!( + run_file(&src), + lines(&["5", "9", "false", "true", "true", "true", "7", "true", "true"]) + ); +} + +#[test] +fn test_optmethods_1_03_ok_methods() { + let src = main_of( + r#" let r = Ok(2) + println(r.unwrap()) + println(r.expect("need ok")) + println(r.unwrap_or(0)) + println(r.unwrap_or_else(|e| 0)) + println(r.is_ok()) + println(r.is_err()) + println(r.map(|x| x + 1).unwrap()) + println(r.and_then(|x| Ok(x * 10)).unwrap()) + println(r.ok().unwrap()) + println(r.or(Ok(9)).unwrap())"#, + ); + assert_eq!( + run_file(&src), + lines(&["2", "2", "2", "2", "true", "false", "3", "20", "2", "2"]) + ); +} + +#[test] +fn test_optmethods_1_04_err_methods() { + let src = main_of( + r#" let r = Err("bad") + println(r.unwrap_or(0)) + println(r.unwrap_or_else(|e| 1)) + println(r.is_ok()) + println(r.is_err()) + println(r.map(|x| x + 1).is_err()) + println(r.and_then(|x| Ok(x)).is_err()) + println(r.ok().is_none()) + println(r.or(Ok(9)).unwrap())"#, + ); + assert_eq!( + run_file(&src), + lines(&["0", "1", "false", "true", "true", "true", "true", "9"]) + ); +} + +#[test] +fn test_optmethods_1_05_plain_value_is_present() { + let src = main_of( + r#" let x = 5 + println(x.unwrap()) + println(x.expect("need x")) + println(x.unwrap_or(0)) + println(x.is_some()) + println(x.is_none()) + println(x.and_then(|v| v + 1)) + println("hi".unwrap()) + let v = [1, 2, 3] + println(v.first().unwrap())"#, + ); + assert_eq!( + run_file(&src), + lines(&["5", "5", "5", "true", "false", "6", "hi", "1"]) + ); +} + +#[test] +fn test_optmethods_1_06_plain_value_methods_not_shadowed() { + let src = main_of( + r#" let v = [1, 2, 3] + println(v.map(|x| x * 2)) + println(v.filter(|x| x > 1)) + println("abc".len()) + println(7.abs()) + let m = {"a": 1} + println(m.keys())"#, + ); + assert_eq!( + run_file(&src), + lines(&["[2, 4, 6]", "[2, 3]", "3", "7", "[\"a\"]"]) + ); +} + +#[test] +fn test_optmethods_1_07_nil_is_absent() { + let src = main_of( + r#" let mut e = [1] + e.pop() + let p = e.pop() + println(p.is_none()) + println(p.is_some()) + println(p.unwrap_or(4)) + println(p.unwrap_or_else(|| 8)) + println(p.map(|x| x + 1).is_none()) + println(p.or(Some(2)).unwrap())"#, + ); + assert_eq!( + run_file(&src), + lines(&["true", "false", "4", "8", "true", "2"]) + ); +} + +#[test] +fn test_optmethods_1_08_unwrap_on_absent_is_an_error_naming_the_value() { + let none = run_failing(&main_of(" let n = None\n println(n.unwrap())")); + assert!(none.contains("unwrap") && none.contains("None"), "{none}"); + let err = run_failing(&main_of( + " let r = Err(\"boom\")\n println(r.unwrap())", + )); + assert!(err.contains("unwrap") && err.contains("boom"), "{err}"); + let nil = run_failing(&main_of( + " let mut e = []\n let p = e.pop()\n println(p.unwrap())", + )); + assert!(nil.contains("unwrap") && nil.contains("nil"), "{nil}"); +} + +#[test] +fn test_optmethods_1_09_expect_on_absent_includes_the_message() { + let out = run_failing(&main_of( + " let n = None\n println(n.expect(\"value was required\"))", + )); + assert!(out.contains("value was required"), "{out}"); + let err = run_failing(&main_of( + " let r = Err(\"boom\")\n println(r.expect(\"result was required\"))", + )); + assert!( + err.contains("result was required") && err.contains("boom"), + "{err}" + ); +} + +#[test] +fn test_optmethods_1_10_unwrap_or_default_on_absent_is_an_error_not_nil() { + let out = run_failing(&main_of( + " let n = None\n println(n.unwrap_or_default())", + )); + assert!( + out.contains("unwrap_or_default") && out.contains("None") && !out.contains("not found"), + "{out}" + ); +} + +const STACK: &str = "struct Stack { items: Vec } +impl Stack { + fun pop_one(&mut self) -> i32 { + self.items.pop().unwrap() + } +} +"; + +#[test] +fn test_optmethods_1_11_field_pop_unwrap_in_method() { + let src = format!( + "{STACK}fun main() {{ + let mut s = Stack {{ items: vec![1, 2, 3] }} + println(s.pop_one()) + println(s.pop_one()) +}} +" + ); + assert_eq!(run_file(&src), "3\n2\n"); +} + +#[test] +fn test_optmethods_1_12_map_get_on_plain_object() { + let src = main_of( + r#" let m = {"a": 1, "b": 2} + println(m.get("a")) + println(m.get("a").unwrap()) + println(m.get("z").is_none()) + println(m.get("z").unwrap_or(0)) + println(m.values())"#, + ); + assert_eq!(run_file(&src), lines(&["1", "1", "true", "0", "[1, 2]"])); +} + +#[test] +fn test_optmethods_1_13_differential_pop_unwrap_through_rustc() { + let src = format!( + "{STACK}fun main() {{ + let mut s = Stack {{ items: vec![1, 2, 3] }} + println(s.pop_one()) + println(s.pop_one()) + println(s.items.pop().unwrap_or(0)) + println(s.items.pop().unwrap_or(0)) + println(s.items.pop().is_none()) +}} +" + ); + assert_eq!(differential(&src), "3\n2\n1\n0\ntrue\n"); +} From 2f5eb9e28b4559f777967f7d2ce65d70569153f6 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:56:04 +0200 Subject: [PATCH 045/106] [RHLGA-1] OPTMETHODS-1: Option/Result methods and map.get in the interpreter - new src/runtime/eval_option_methods.rs: unwrap, expect, unwrap_or, unwrap_or_else, unwrap_or_default, is_some/is_none/is_ok/is_err, map, and_then, ok, ok_or, or, filter. Some(v)/Ok(v)/plain non-nil are present; None/Err(e)/nil are absent. unwrap/expect/unwrap_or_default on an absent value error and name it (expect includes its message). - interpreter dispatch: Some/None/Ok/Err/nil receivers try these first; any other value reaches them only after its own type reports the method unknown, so no string/array/integer/object method is shadowed. - object/object-mut dispatch arms extracted into helpers keyed by an ObjectKind marker (the renamed native dispatcher was cognitive 28). - plain object get(key): the value or nil. - tests/optmethods_1_option_methods.rs 13/13, fieldpop_1 12/12, lib 20845 passed Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 9 + src/runtime/eval_method_dispatch.rs | 38 +- src/runtime/eval_option_methods.rs | 391 ++++++++++++++++++++ src/runtime/interpreter_methods_dispatch.rs | 228 +++++++----- src/runtime/mod.rs | 1 + 5 files changed, 572 insertions(+), 95 deletions(-) create mode 100644 src/runtime/eval_option_methods.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c5fc72de..e4114266e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,15 @@ Behaviour changes a program can observe are marked **(behaviour)**. `ruchy infra …` rejects a missing path or a directory. `ruchy serve` returns an error when it cannot bind its port, and `--watch` keeps serving, with a warning, when the host has no inotify instance left. +- **(behaviour)** The interpreter has `Option`/`Result` methods again + (OPTMETHODS-1): `unwrap`, `expect`, `unwrap_or`, `unwrap_or_else`, + `unwrap_or_default`, `is_some`/`is_none`/`is_ok`/`is_err`, `map`, + `and_then`, `ok`, `ok_or`, `or`, `filter`. `Some(v)`, `Ok(v)` and a plain + non-nil value are present; `None`, `Err(e)` and nil are absent, so + `v.pop().unwrap()` yields the element. A plain value's own methods win. + `unwrap`/`expect` on an absent value is an error naming it, and so is + `unwrap_or_default`, whose default type is unknown at runtime. A plain + object has `get(key)`, returning the value or nil. - Tests: - they spawn the cargo-built binary; - they write compiled output (`a.out`, `.rlib`) into temp dirs, never the repository; diff --git a/src/runtime/eval_method_dispatch.rs b/src/runtime/eval_method_dispatch.rs index bd7babf29..3eb6aee56 100644 --- a/src/runtime/eval_method_dispatch.rs +++ b/src/runtime/eval_method_dispatch.rs @@ -7,6 +7,7 @@ use crate::frontend::ast::Expr; use crate::runtime::eval_array; +use crate::runtime::eval_option_methods; use crate::runtime::eval_string; use crate::runtime::interpreter::DataFrameColumn; use crate::runtime::{InterpreterError, Value}; @@ -92,7 +93,13 @@ where arg_values, ), Value::Object(obj) => eval_object_method(obj, base_method, arg_values), - _ => eval_generic_method(receiver, base_method, args_empty), + _ => eval_option_methods::eval_option_method( + receiver, + base_method, + arg_values, + &mut eval_function_call_value, + ) + .unwrap_or_else(|| eval_generic_method(receiver, base_method, args_empty)), } } @@ -229,18 +236,43 @@ fn try_dispatch_builtin( } } -/// METHODS-1: `keys`, `values`, `items`/`entries` on a plain object (no `__type`). +/// OPTMETHODS-1: `map.get(key)` on a plain object: the value, or nil if absent. +/// +/// # Complexity +/// Cyclomatic complexity: 3 +fn eval_plain_object_get( + obj: &std::collections::HashMap, + arg_values: &[Value], +) -> Result { + let [key] = arg_values else { + return Err(InterpreterError::RuntimeError(format!( + "Object method 'get' takes 1 argument, got {}", + arg_values.len() + ))); + }; + let key = match key { + Value::String(s) => s.to_string(), + other => other.to_string(), + }; + Ok(obj.get(&key).cloned().unwrap_or(Value::Nil)) +} + +/// METHODS-1: `keys`, `values`, `items`/`entries` on a plain object (no `__type`), +/// and `get` (OPTMETHODS-1). /// /// Entries come back in sorted key order, the order `Display` prints an object in. /// Returns `None` for any other method so the caller keeps its error path. /// /// # Complexity -/// Cyclomatic complexity: 5 +/// Cyclomatic complexity: 6 fn eval_plain_object_method( obj: &std::collections::HashMap, method: &str, arg_values: &[Value], ) -> Option> { + if method == "get" { + return Some(eval_plain_object_get(obj, arg_values)); + } let project: fn(&String, &Value) -> Value = match method { "keys" => |k, _| Value::from_string(k.clone()), "values" => |_, v| v.clone(), diff --git a/src/runtime/eval_option_methods.rs b/src/runtime/eval_option_methods.rs new file mode 100644 index 000000000..e4150c531 --- /dev/null +++ b/src/runtime/eval_option_methods.rs @@ -0,0 +1,391 @@ +//! OPTMETHODS-1: `Option`/`Result` methods in the interpreter. +//! +//! The interpreter keeps nil-for-absent library results (`pop`, `first`, +//! `get` return the element or nil), so these methods accept both forms: +//! `Some(v)`/`Ok(v)` and a plain non-nil value are present; `None`, `Err(e)` +//! and nil are absent. `v.pop().unwrap()` therefore yields the element, as +//! the transpiled Rust does. +//! +//! A plain value only reaches these methods after its own type has reported +//! the method as unknown (see [`is_unknown_method_error`]), so an existing +//! string/array/integer/object method is never shadowed. + +use crate::runtime::{InterpreterError, Value}; + +/// Which constructor a present value is wrapped in when a method rebuilds it. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Family { + Option, + Result, + Plain, +} + +/// A receiver seen as an `Option`/`Result`. +enum Shape { + /// `Some(v)`, `Ok(v)` or a plain non-nil value. + Present(Family, Value), + /// `None` or nil (payload `Nil`), or `Err(e)` (payload `e`). + Absent(Family, Value), +} + +/// Result of an `Option` method dispatch; `None` means "not an `Option` method". +pub type OptionMethodResult = Option>; + +/// True if `receiver` is `Some`/`None`/`Ok`/`Err` or nil: these dispatch here +/// before any other method table. +/// +/// # Complexity +/// Cyclomatic complexity: 2 +pub fn is_option_receiver(receiver: &Value) -> bool { + matches!(receiver, Value::Nil) || variant_payload(receiver).is_some() +} + +/// True if `err` is a type's "no such method" error for `method`. +/// +/// # Complexity +/// Cyclomatic complexity: 4 +pub fn is_unknown_method_error(err: &InterpreterError, method: &str) -> bool { + let InterpreterError::RuntimeError(msg) = err else { + return false; + }; + let names_method = + msg.ends_with(&format!(": {method}")) || msg.contains(&format!("'{method}'")); + names_method && (msg.contains("Unknown") || msg.contains("not found")) +} + +/// The variant name and payload of a `Some`/`None`/`Ok`/`Err` value. +/// +/// # Complexity +/// Cyclomatic complexity: 3 +fn variant_payload(receiver: &Value) -> Option<(&str, Option<&Value>)> { + let Value::EnumVariant { + variant_name, data, .. + } = receiver + else { + return None; + }; + let payload = data.as_ref().and_then(|d| d.first()); + match variant_name.as_str() { + "Some" | "None" | "Ok" | "Err" => Some((variant_name.as_str(), payload)), + _ => None, + } +} + +/// # Complexity +/// Cyclomatic complexity: 7 +fn classify(receiver: &Value) -> Shape { + let payload = |p: Option<&Value>| p.cloned().unwrap_or(Value::Nil); + match variant_payload(receiver) { + Some(("Some", p)) => Shape::Present(Family::Option, payload(p)), + Some(("Ok", p)) => Shape::Present(Family::Result, payload(p)), + Some(("Err", p)) => Shape::Absent(Family::Result, payload(p)), + Some(_) => Shape::Absent(Family::Option, Value::Nil), + None if matches!(receiver, Value::Nil) => Shape::Absent(Family::Plain, Value::Nil), + None => Shape::Present(Family::Plain, receiver.clone()), + } +} + +fn variant(enum_name: &str, variant_name: &str, data: Option) -> Value { + Value::EnumVariant { + enum_name: enum_name.to_string(), + variant_name: variant_name.to_string(), + data: data.map(|v| vec![v]), + } +} + +fn some(v: Value) -> Value { + variant("Option", "Some", Some(v)) +} + +fn none() -> Value { + variant("Option", "None", None) +} + +/// Rebuild a present value in its family's constructor. +fn wrap(family: Family, v: Value) -> Value { + match family { + Family::Option => some(v), + Family::Result => variant("Result", "Ok", Some(v)), + Family::Plain => v, + } +} + +/// The absent value of a family that has no payload to keep. +fn empty(family: Family) -> Value { + match family { + Family::Plain => Value::Nil, + _ => none(), + } +} + +/// How an absent receiver is named in error messages. +fn describe_absent(family: Family, payload: &Value) -> String { + match family { + Family::Option => "None".to_string(), + Family::Result => format!("Err({payload})"), + Family::Plain => "nil".to_string(), + } +} + +fn arg(method: &str, args: &[Value], count: usize) -> Result<(), InterpreterError> { + if args.len() == count { + return Ok(()); + } + Err(InterpreterError::RuntimeError(format!( + "{method}() takes {count} argument(s), got {}", + args.len() + ))) +} + +/// Evaluate an `Option`/`Result` method on `receiver`, or `None` if `method` +/// is not one of them. `call` invokes a closure argument. +/// +/// # Complexity +/// Cyclomatic complexity: 3 +pub fn eval_option_method( + receiver: &Value, + method: &str, + args: &[Value], + call: &mut F, +) -> OptionMethodResult +where + F: FnMut(&Value, &[Value]) -> Result, +{ + let shape = classify(receiver); + if let Some(result) = eval_query_method(&shape, method, args) { + return Some(result); + } + eval_combinator_method(receiver, shape, method, args, call) +} + +/// Predicates and extractors that never call a closure. +/// +/// # Complexity +/// Cyclomatic complexity: 9 +fn eval_query_method(shape: &Shape, method: &str, args: &[Value]) -> OptionMethodResult { + let present = matches!(shape, Shape::Present(..)); + let result = match method { + "is_some" | "is_ok" => arg(method, args, 0).map(|()| Value::Bool(present)), + "is_none" | "is_err" => arg(method, args, 0).map(|()| Value::Bool(!present)), + "unwrap" => arg(method, args, 0).and_then(|()| unwrap(shape, method, None)), + "expect" => arg(method, args, 1).and_then(|()| unwrap(shape, method, Some(&args[0]))), + "unwrap_or_default" => arg(method, args, 0).and_then(|()| unwrap_or_default(shape)), + "unwrap_or" => arg(method, args, 1).map(|()| present_or(shape, &args[0])), + "ok" => arg(method, args, 0).map(|()| ok(shape)), + "ok_or" => arg(method, args, 1).map(|()| ok_or(shape, &args[0])), + _ => return None, + }; + Some(result) +} + +/// `unwrap`/`expect`: the present value, or an error naming the absent one. +fn unwrap(shape: &Shape, method: &str, msg: Option<&Value>) -> Result { + match (shape, msg) { + (Shape::Present(_, v), _) => Ok(v.clone()), + (Shape::Absent(family, p), None) => Err(InterpreterError::RuntimeError(format!( + "called `{method}()` on {}", + describe_absent(*family, p) + ))), + (Shape::Absent(family, p), Some(msg)) => Err(InterpreterError::RuntimeError(format!( + "{}: {}", + display_text(msg), + describe_absent(*family, p) + ))), + } +} + +fn display_text(v: &Value) -> String { + match v { + Value::String(s) => s.to_string(), + other => other.to_string(), + } +} + +/// The default of an absent value's type is not known at runtime, so an +/// absent receiver is an error rather than a guessed default. +fn unwrap_or_default(shape: &Shape) -> Result { + match shape { + Shape::Present(_, v) => Ok(v.clone()), + Shape::Absent(family, p) => Err(InterpreterError::RuntimeError(format!( + "called `unwrap_or_default()` on {}: the default value's type is unknown at runtime", + describe_absent(*family, p) + ))), + } +} + +fn present_or(shape: &Shape, fallback: &Value) -> Value { + match shape { + Shape::Present(_, v) => v.clone(), + Shape::Absent(..) => fallback.clone(), + } +} + +fn ok(shape: &Shape) -> Value { + match shape { + Shape::Present(_, v) => some(v.clone()), + Shape::Absent(..) => none(), + } +} + +fn ok_or(shape: &Shape, err: &Value) -> Value { + match shape { + Shape::Present(_, v) => variant("Result", "Ok", Some(v.clone())), + Shape::Absent(..) => variant("Result", "Err", Some(err.clone())), + } +} + +/// Methods that take a closure or return the receiver itself. +/// +/// # Complexity +/// Cyclomatic complexity: 8 +fn eval_combinator_method( + receiver: &Value, + shape: Shape, + method: &str, + args: &[Value], + call: &mut F, +) -> OptionMethodResult +where + F: FnMut(&Value, &[Value]) -> Result, +{ + let result = match method { + "or" => arg(method, args, 1).map(|()| or(receiver, &shape, &args[0])), + "unwrap_or_else" => { + arg(method, args, 1).and_then(|()| unwrap_or_else(shape, &args[0], call)) + } + "map" => arg(method, args, 1).and_then(|()| map(receiver, shape, &args[0], call)), + "and_then" => arg(method, args, 1).and_then(|()| and_then(receiver, shape, &args[0], call)), + "filter" => arg(method, args, 1).and_then(|()| filter(receiver, shape, &args[0], call)), + _ => return None, + }; + Some(result) +} + +fn or(receiver: &Value, shape: &Shape, fallback: &Value) -> Value { + match shape { + Shape::Present(..) => receiver.clone(), + Shape::Absent(..) => fallback.clone(), + } +} + +/// `Err(e).unwrap_or_else(f)` passes `e` to `f`; `None`/nil pass nothing. +fn unwrap_or_else(shape: Shape, f: &Value, call: &mut F) -> Result +where + F: FnMut(&Value, &[Value]) -> Result, +{ + match shape { + Shape::Present(_, v) => Ok(v), + Shape::Absent(Family::Result, e) => call(f, &[e]), + Shape::Absent(..) => call(f, &[]), + } +} + +fn map( + receiver: &Value, + shape: Shape, + f: &Value, + call: &mut F, +) -> Result +where + F: FnMut(&Value, &[Value]) -> Result, +{ + match shape { + Shape::Present(family, v) => Ok(wrap(family, call(f, &[v])?)), + Shape::Absent(..) => Ok(receiver.clone()), + } +} + +fn and_then( + receiver: &Value, + shape: Shape, + f: &Value, + call: &mut F, +) -> Result +where + F: FnMut(&Value, &[Value]) -> Result, +{ + match shape { + Shape::Present(_, v) => call(f, &[v]), + Shape::Absent(..) => Ok(receiver.clone()), + } +} + +fn filter( + receiver: &Value, + shape: Shape, + pred: &Value, + call: &mut F, +) -> Result +where + F: FnMut(&Value, &[Value]) -> Result, +{ + match shape { + Shape::Present(family, v) => { + let keep = call(pred, &[v])?.is_truthy(); + Ok(if keep { + receiver.clone() + } else { + empty(family) + }) + } + Shape::Absent(..) => Ok(receiver.clone()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn no_call(_: &Value, _: &[Value]) -> Result { + Err(InterpreterError::RuntimeError("no closure".to_string())) + } + + fn eval(receiver: &Value, method: &str, args: &[Value]) -> Result { + eval_option_method(receiver, method, args, &mut no_call).expect("an option method") + } + + #[test] + fn test_optmethods_1_unit_present_and_absent_forms() { + let some3 = some(Value::Integer(3)); + assert_eq!(eval(&some3, "unwrap", &[]).unwrap(), Value::Integer(3)); + assert_eq!( + eval(&Value::Integer(4), "unwrap", &[]).unwrap(), + Value::Integer(4) + ); + assert_eq!(eval(&none(), "is_none", &[]).unwrap(), Value::Bool(true)); + assert_eq!( + eval(&Value::Nil, "is_some", &[]).unwrap(), + Value::Bool(false) + ); + let fallback = [Value::Integer(5)]; + assert_eq!( + eval(&Value::Nil, "unwrap_or", &fallback).unwrap(), + Value::Integer(5) + ); + } + + #[test] + fn test_optmethods_1_unit_absent_unwrap_errors_name_the_value() { + let err = variant( + "Result", + "Err", + Some(Value::from_string("boom".to_string())), + ); + let msg = eval(&err, "unwrap", &[]).unwrap_err().to_string(); + assert!(msg.contains("boom"), "{msg}"); + let msg = eval(&Value::Nil, "unwrap", &[]).unwrap_err().to_string(); + assert!(msg.contains("nil"), "{msg}"); + assert!(eval(&none(), "unwrap_or_default", &[]).is_err()); + } + + #[test] + fn test_optmethods_1_unit_non_option_method_and_receiver() { + assert!(eval_option_method(&none(), "len", &[], &mut no_call).is_none()); + assert!(is_option_receiver(&Value::Nil)); + assert!(!is_option_receiver(&Value::Integer(1))); + let unknown = InterpreterError::RuntimeError("Unknown integer method: unwrap".to_string()); + assert!(is_unknown_method_error(&unknown, "unwrap")); + let other = InterpreterError::RuntimeError("Unknown integer method: map_x".to_string()); + assert!(!is_unknown_method_error(&other, "map")); + } +} diff --git a/src/runtime/interpreter_methods_dispatch.rs b/src/runtime/interpreter_methods_dispatch.rs index f25950317..495ddf630 100644 --- a/src/runtime/interpreter_methods_dispatch.rs +++ b/src/runtime/interpreter_methods_dispatch.rs @@ -15,6 +15,36 @@ use crate::runtime::interpreter::Interpreter; use crate::runtime::{InterpreterError, Value}; use std::sync::Arc; +/// Which handler an object's dispatch marker selects, checked in the order +/// actor, class, struct, `__type`. +enum ObjectKind { + Actor(String), + Class(String), + Struct(String), + Typed(String), + Plain, +} + +impl ObjectKind { + /// Complexity: 5 + fn of(obj: &std::collections::HashMap) -> Self { + let marker = |key: &str| match obj.get(key) { + Some(Value::String(s)) => Some(s.to_string()), + _ => None, + }; + if let Some(name) = marker("__actor") { + return Self::Actor(name); + } + if let Some(name) = marker("__class") { + return Self::Class(name); + } + if let Some(name) = marker("__struct_type").or_else(|| marker("__struct")) { + return Self::Struct(name); + } + marker("__type").map_or(Self::Plain, Self::Typed) + } +} + /// FIELDPOP-1: array methods that change their receiver in place — the same /// set the interpreter mutates for a local array receiver (`a.push(x)`, `a.pop()`). fn is_mutating_array_call(method: &str, arg_count: usize) -> bool { @@ -292,12 +322,55 @@ impl Interpreter { } } + /// Dispatch a method call on an evaluated receiver. + /// + /// OPTMETHODS-1: `Some`/`None`/`Ok`/`Err`/nil receivers try the `Option` + /// methods first; any other receiver reaches them only after its own type + /// reported the method unknown, so no existing method is shadowed. + /// + /// Complexity: 4 pub(crate) fn dispatch_method_call( &mut self, receiver: &Value, method: &str, arg_values: &[Value], args_empty: bool, + ) -> Result { + use super::eval_option_methods::{is_option_receiver, is_unknown_method_error}; + let base_method = method.split("::").next().unwrap_or(method); + if is_option_receiver(receiver) { + if let Some(result) = self.eval_option_method(receiver, base_method, arg_values) { + return result; + } + } + let result = self.dispatch_native_method_call(receiver, method, arg_values, args_empty); + match result { + Err(ref e) if is_unknown_method_error(e, base_method) => self + .eval_option_method(receiver, base_method, arg_values) + .unwrap_or(result), + _ => result, + } + } + + /// OPTMETHODS-1: `Option`/`Result` method with closure arguments called + /// through the interpreter. `None` if `method` is not one of them. + fn eval_option_method( + &mut self, + receiver: &Value, + method: &str, + arg_values: &[Value], + ) -> super::eval_option_methods::OptionMethodResult { + let mut call = |f: &Value, args: &[Value]| self.call_function(f.clone(), args); + super::eval_option_methods::eval_option_method(receiver, method, arg_values, &mut call) + } + + /// Method tables of the receiver's own type (no `Option` bridge). + fn dispatch_native_method_call( + &mut self, + receiver: &Value, + method: &str, + arg_values: &[Value], + args_empty: bool, ) -> Result { // EVALUATOR-001: Strip turbofish syntax from method names // Example: "parse::" becomes "parse" @@ -317,100 +390,10 @@ impl Interpreter { self.eval_dataframe_method(columns, base_method, arg_values) } Value::Object(obj) => { - // Check if this is an actor instance - if let Some(Value::String(actor_name)) = obj.get("__actor") { - self.eval_actor_instance_method( - obj, - actor_name.as_ref(), - base_method, - arg_values, - ) - } - // Check if this is a class instance - else if let Some(Value::String(class_name)) = obj.get("__class") { - self.eval_class_instance_method( - obj, - class_name.as_ref(), - base_method, - arg_values, - ) - } - // Check if this is a struct instance with impl methods - else if let Some(Value::String(struct_name)) = - obj.get("__struct_type").or_else(|| obj.get("__struct")) - { - self.eval_struct_instance_method( - obj, - struct_name.as_ref(), - base_method, - arg_values, - ) - } - // Check if this is a `DataFrame` builder - else if let Some(Value::String(type_str)) = obj.get("__type") { - if type_str.as_ref() == "DataFrameBuilder" { - self.eval_dataframe_builder_method(obj, base_method, arg_values) - } else { - self.eval_object_method(obj, base_method, arg_values, args_empty) - } - } else { - self.eval_object_method(obj, base_method, arg_values, args_empty) - } + self.dispatch_plain_object_method(obj, base_method, arg_values, args_empty) } Value::ObjectMut(cell_rc) => { - // Dispatch mutable objects the same way as immutable ones - // Safe borrow: We only read metadata fields to determine dispatch - let obj = cell_rc - .lock() - .expect("Mutex poisoned: object lock is corrupted"); - - // Check if this is an actor instance - if let Some(Value::String(actor_name)) = obj.get("__actor") { - let actor_name = actor_name.clone(); - drop(obj); // Release borrow before recursive call - self.eval_actor_instance_method_mut( - cell_rc, - actor_name.as_ref(), - base_method, - arg_values, - ) - } - // Check if this is a class instance - else if let Some(Value::String(class_name)) = obj.get("__class") { - let class_name = class_name.clone(); - drop(obj); // Release borrow before recursive call - self.eval_class_instance_method_mut( - cell_rc, - class_name.as_ref(), - base_method, - arg_values, - ) - } - // Check if this is a struct instance with impl methods - else if let Some(Value::String(struct_name)) = - obj.get("__struct_type").or_else(|| obj.get("__struct")) - { - let struct_name = struct_name.clone(); - drop(obj); // Release borrow before recursive call - self.eval_struct_instance_method_mut( - cell_rc, - struct_name.as_ref(), - base_method, - arg_values, - ) - } - // ISSUE-116: Check if this is a File object - else if let Some(Value::String(type_name)) = obj.get("__type") { - if type_name.as_ref() == "File" { - drop(obj); // Release borrow before recursive call - return self.eval_file_method_mut(cell_rc, base_method, arg_values); - } - drop(obj); // Release borrow before recursive call - self.eval_object_method_mut(cell_rc, base_method, arg_values, args_empty) - } else { - drop(obj); // Release borrow before recursive call - self.eval_object_method_mut(cell_rc, base_method, arg_values, args_empty) - } + self.dispatch_object_mut_method(cell_rc, base_method, arg_values, args_empty) } Value::Struct { name, fields } => { // Dispatch struct instance method call @@ -442,6 +425,67 @@ impl Interpreter { } } + /// Dispatch a method on an immutable object by its dispatch marker. + /// + /// Complexity: 5 + fn dispatch_plain_object_method( + &mut self, + obj: &std::collections::HashMap, + method: &str, + arg_values: &[Value], + args_empty: bool, + ) -> Result { + match ObjectKind::of(obj) { + ObjectKind::Actor(name) => { + self.eval_actor_instance_method(obj, &name, method, arg_values) + } + ObjectKind::Class(name) => { + self.eval_class_instance_method(obj, &name, method, arg_values) + } + ObjectKind::Struct(name) => { + self.eval_struct_instance_method(obj, &name, method, arg_values) + } + ObjectKind::Typed(name) if name == "DataFrameBuilder" => { + self.eval_dataframe_builder_method(obj, method, arg_values) + } + _ => self.eval_object_method(obj, method, arg_values, args_empty), + } + } + + /// Dispatch a method on a mutable object by its dispatch marker. The lock + /// is released before the handler runs. + /// + /// Complexity: 5 + fn dispatch_object_mut_method( + &mut self, + cell_rc: &std::sync::Arc>>, + method: &str, + arg_values: &[Value], + args_empty: bool, + ) -> Result { + let kind = ObjectKind::of( + &cell_rc + .lock() + .expect("Mutex poisoned: object lock is corrupted"), + ); + match kind { + ObjectKind::Actor(name) => { + self.eval_actor_instance_method_mut(cell_rc, &name, method, arg_values) + } + ObjectKind::Class(name) => { + self.eval_class_instance_method_mut(cell_rc, &name, method, arg_values) + } + ObjectKind::Struct(name) => { + self.eval_struct_instance_method_mut(cell_rc, &name, method, arg_values) + } + // ISSUE-116: File objects + ObjectKind::Typed(name) if name == "File" => { + self.eval_file_method_mut(cell_rc, method, arg_values) + } + _ => self.eval_object_method_mut(cell_rc, method, arg_values, args_empty), + } + } + pub(crate) fn eval_float_method( &self, f: f64, diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 9e02ef93b..595732623 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -107,6 +107,7 @@ pub mod eval_loops; pub mod eval_method; pub mod eval_method_dispatch; pub mod eval_operations; +pub mod eval_option_methods; // OPTMETHODS-1: Option/Result methods pub mod eval_pattern; pub mod eval_pattern_match; pub mod eval_string; From 121a66566679a3c8b36701d7cc99c25be741148c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:48:54 +0200 Subject: [PATCH 046/106] ARRAYMUT-1 (RED): in-place Vec methods must mutate their receiver - tests/arraymut_1_in_place.rs: sort, reverse, append, insert, remove, clear, truncate, dedup on a local array and on self.field; out-of-range insert/remove errors; sorted/reversed stay functional; two differential cases through rustc. 12/15 fail on the current interpreter. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/arraymut_1_in_place.rs | 260 +++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 tests/arraymut_1_in_place.rs diff --git a/tests/arraymut_1_in_place.rs b/tests/arraymut_1_in_place.rs new file mode 100644 index 000000000..aca391a92 --- /dev/null +++ b/tests/arraymut_1_in_place.rs @@ -0,0 +1,260 @@ +//! ARRAYMUT-1: the in-place `Vec` methods (`sort`, `reverse`, `append`, +//! `insert`, `remove`, `clear`, `truncate`, `dedup`, `extend`) change their +//! receiver when it is a local array or a struct field, and return what Rust +//! returns: `()` (printed as `nil`) for all of them except `remove`, which +//! returns the removed element. +//! +//! Before the fix `v.sort()` / `v.reverse()` returned a changed copy and left +//! `v` untouched, and `insert`/`remove`/`clear`/`truncate`/`dedup` failed with +//! "Unknown array method". `sorted`/`reversed` stay functional. +//! +//! `extend(…)` is not exercised here: `.extend(` does not parse yet (tracked +//! as EXTENDKW-1); its interpreter behaviour is covered by lib unit tests in +//! `src/runtime/eval_array.rs`. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn output_of(mut cmd: Command) -> std::process::Output { + cmd.output().expect("spawn command") +} + +fn stdout_of(cmd: Command, what: &str) -> String { + let out = output_of(cmd); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn interpret(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + stdout_of(cmd, "ruchy run") +} + +fn run_file(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + interpret(dir.path(), src) +} + +/// Run a program that must fail; returns its combined stdout+stderr. +fn run_failing(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + let out = output_of(cmd); + assert!(!out.status.success(), "expected failure for:\n{src}"); + format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ) +} + +fn transpile(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, "ruchy transpile") +} + +fn rustc_run(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("main.rs"); + let bin = dir.join("prog_bin"); + std::fs::write(&main_rs, rust).expect("write main.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "transpiled binary") +} + +/// Transpile, compile, run; assert interpreter == rustc output. +fn differential(src: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), src); + let expected = interpret(dir.path(), src); + let actual = rustc_run(dir.path(), &rust); + assert_eq!( + actual, expected, + "transpiled Rust disagrees with the interpreter\nsource:\n{src}\nrust:\n{rust}" + ); +} + +/// `op` on a local `[3, 1, 2, 2]`: prints the call's result, then the array. +fn on_local(op: &str) -> String { + run_file(&format!( + "fun main() {{\n let mut v = [3, 1, 2, 2]\n println(v.{op})\n println(v)\n}}\n" + )) +} + +const BAG: &str = "struct Bag { items: Vec }\n"; + +/// `op` on `self.items` inside a `&mut self` method, then the field. +fn on_self_field(op: &str) -> String { + run_file(&format!( + "{BAG}impl Bag {{ + fun apply(&mut self) {{ + println(self.items.{op}) + }} +}} +fun main() {{ + let mut b = Bag {{ items: vec![3, 1, 2, 2] }} + b.apply() + println(b.items) +}} +" + )) +} + +/// Same op on a local and on `self.items`: both must give `expected`. +fn assert_in_place(op: &str, expected: &str) { + assert_eq!(on_local(op), expected, "local receiver, `{op}`"); + assert_eq!(on_self_field(op), expected, "self.field receiver, `{op}`"); +} + +#[test] +fn test_arraymut_1_01_sort_sorts_receiver() { + assert_in_place("sort()", "nil\n[1, 2, 2, 3]\n"); +} + +#[test] +fn test_arraymut_1_02_reverse_reverses_receiver() { + assert_in_place("reverse()", "nil\n[2, 2, 1, 3]\n"); +} + +#[test] +fn test_arraymut_1_03_insert_places_element() { + assert_in_place("insert(0, 9)", "nil\n[9, 3, 1, 2, 2]\n"); + assert_in_place("insert(4, 9)", "nil\n[3, 1, 2, 2, 9]\n"); +} + +#[test] +fn test_arraymut_1_04_remove_returns_removed_element() { + assert_in_place("remove(1)", "1\n[3, 2, 2]\n"); +} + +#[test] +fn test_arraymut_1_05_clear_empties_receiver() { + assert_in_place("clear()", "nil\n[]\n"); +} + +#[test] +fn test_arraymut_1_06_truncate_shortens_receiver() { + assert_in_place("truncate(2)", "nil\n[3, 1]\n"); + assert_in_place("truncate(10)", "nil\n[3, 1, 2, 2]\n"); +} + +#[test] +fn test_arraymut_1_07_dedup_removes_consecutive_duplicates() { + assert_in_place("dedup()", "nil\n[3, 1, 2]\n"); +} + +#[test] +fn test_arraymut_1_08_append_moves_other_elements_in() { + assert_in_place("append([7, 8])", "nil\n[3, 1, 2, 2, 7, 8]\n"); +} + +#[test] +fn test_arraymut_1_09_field_on_local_struct_is_written_back() { + let src = format!( + "{BAG}fun main() {{ + let mut b = Bag {{ items: vec![3, 1, 2] }} + b.items.sort() + b.items.insert(0, 0) + println(b.items.remove(3)) + println(b.items) +}} +" + ); + assert_eq!(run_file(&src), "3\n[0, 1, 2]\n"); +} + +#[test] +fn test_arraymut_1_10_out_of_range_insert_is_runtime_error() { + let src = "fun main() {\n let mut v = [1, 2]\n v.insert(3, 9)\n println(v)\n}\n"; + let out = run_failing(src); + assert!(out.contains("insert"), "error names the method: {out}"); +} + +#[test] +fn test_arraymut_1_11_out_of_range_remove_is_runtime_error() { + let src = "fun main() {\n let mut v = [1, 2]\n v.remove(2)\n println(v)\n}\n"; + let out = run_failing(src); + assert!(out.contains("remove"), "error names the method: {out}"); +} + +#[test] +fn test_arraymut_1_12_sorted_and_reversed_stay_functional() { + let src = "fun main() {\n let v = [3, 1, 2]\n println(v.sorted())\n println(v.reversed())\n println(v)\n}\n"; + assert_eq!(run_file(src), "[1, 2, 3]\n[2, 1, 3]\n[3, 1, 2]\n"); +} + +#[test] +fn test_arraymut_1_13_mutation_inside_loop_persists() { + let src = "fun main() {\n let mut v = [5, 4, 3, 2, 1]\n let mut i = 0\n while i < 2 {\n v.remove(0)\n i = i + 1\n }\n v.sort()\n println(v)\n}\n"; + assert_eq!(run_file(src), "[1, 2, 3]\n"); +} + +#[test] +fn test_arraymut_1_14_differential_local_vec() { + differential( + "fun main() { + let mut v = vec![3, 1, 2, 2] + v.sort() + v.dedup() + v.reverse() + v.insert(0, 9) + let r = v.remove(1) + v.truncate(3) + println(\"{} {:?}\", r, v) + v.clear() + println(\"{}\", v.len()) +} +", + ); +} + +#[test] +fn test_arraymut_1_15_differential_self_field() { + differential(&format!( + "{BAG}impl Bag {{ + fun tidy(&mut self) -> i32 {{ + self.items.sort() + self.items.dedup() + self.items.insert(0, 7) + self.items.truncate(3) + self.items.remove(0) + }} +}} +fun main() {{ + let mut b = Bag {{ items: vec![3, 1, 2, 2, 5] }} + let r = b.tidy() + println(\"{{}} {{:?}}\", r, b.items) +}} +" + )); +} From 3e2a47707fadea6a9627411f4ac6f6d99409e4cc Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:53:44 +0200 Subject: [PATCH 047/106] ARRAYMUT-1 (GREEN): in-place Vec methods mutate a local or field receiver - eval_array.rs: is_in_place_array_method (table: push, pop, sort, reverse, append, extend, insert, remove, clear, truncate, dedup) and apply_in_place_array_method returning Rust's result (remove -> removed element, pop -> element or nil, all others nil). Out-of-range insert/remove and non-integer indices are runtime errors; extend and append take an array, tuple or integer range. 7 unit tests, including extend (its call syntax does not parse yet, EXTENDKW-1). - interpreter_methods_dispatch.rs: the push/pop identifier special case becomes try_local_array_mutation over the table; FIELDPOP-1's try_field_array_mutation uses the same table. sorted/reversed and the functional array methods are unchanged. - tests/arraymut_1_in_place.rs 15/15, fieldpop_1_field_methods 12/12, lib 20849 passed. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/runtime/eval_array.rs | 259 ++++++++++++++++++++ src/runtime/interpreter_methods_dispatch.rs | 104 ++++---- 2 files changed, 313 insertions(+), 50 deletions(-) diff --git a/src/runtime/eval_array.rs b/src/runtime/eval_array.rs index dc3db687f..78816c1f5 100644 --- a/src/runtime/eval_array.rs +++ b/src/runtime/eval_array.rs @@ -110,6 +110,163 @@ where } } +/// ARRAYMUT-1: array methods that change their receiver in place, as `Vec` +/// methods do in Rust. The interpreter applies them to a local array or a +/// field and writes the changed array back (see `apply_in_place_array_method`). +/// +/// # Complexity +/// Cyclomatic complexity: 1 +pub(crate) fn is_in_place_array_method(method: &str, arg_count: usize) -> bool { + matches!( + (method, arg_count), + ("push" | "append" | "extend" | "remove" | "truncate", 1) + | ("pop" | "sort" | "reverse" | "clear" | "dedup", 0) + | ("insert", 2) + ) +} + +/// ARRAYMUT-1: apply an in-place method to `items`, returning what the Rust +/// `Vec` method returns: the removed element for `remove`, the popped element +/// (or `nil` when empty) for `pop`, and `nil` for every other method. +/// +/// # Errors +/// Out-of-range `insert`/`remove`, a non-integer index, a non-iterable +/// `append`/`extend` argument, or a method outside `is_in_place_array_method`. +/// +/// # Complexity +/// Cyclomatic complexity: 2 +pub(crate) fn apply_in_place_array_method( + items: &mut Vec, + method: &str, + args: &[Value], +) -> Result { + match args { + [] => apply_in_place_nullary(items, method), + _ => apply_in_place_with_args(items, method, args), + } +} + +/// # Complexity +/// Cyclomatic complexity: 7 +fn apply_in_place_nullary(items: &mut Vec, method: &str) -> Result { + match method { + "pop" => return Ok(items.pop().unwrap_or(Value::Nil)), + "sort" => items.sort_by(compare_for_sort), + "reverse" => items.reverse(), + "clear" => items.clear(), + "dedup" => items.dedup_by(|a, b| values_equal(a, b)), + _ => return Err(unknown_in_place(method)), + } + Ok(Value::Nil) +} + +/// # Complexity +/// Cyclomatic complexity: 7 +fn apply_in_place_with_args( + items: &mut Vec, + method: &str, + args: &[Value], +) -> Result { + match (method, args) { + ("push", [item]) => items.push(item.clone()), + ("append" | "extend", [other]) => items.extend(iterable_items(method, other)?), + ("remove", [index]) => return remove_at(items, index), + ("truncate", [len]) => items.truncate(index_arg("truncate", len)?), + ("insert", [index, item]) => insert_at(items, index, item)?, + _ => return Err(unknown_in_place(method)), + } + Ok(Value::Nil) +} + +fn unknown_in_place(method: &str) -> InterpreterError { + InterpreterError::RuntimeError(format!("Unknown array method: {method}")) +} + +/// A non-negative integer argument used as an index or length. +/// +/// # Complexity +/// Cyclomatic complexity: 3 +fn index_arg(method: &str, value: &Value) -> Result { + match value { + Value::Integer(i) => usize::try_from(*i).map_err(|_| { + InterpreterError::RuntimeError(format!("{method}: index must be non-negative, got {i}")) + }), + other => Err(InterpreterError::RuntimeError(format!( + "{method}: index must be an integer, got {}", + other.type_name() + ))), + } +} + +/// # Complexity +/// Cyclomatic complexity: 2 +fn remove_at(items: &mut Vec, index: &Value) -> Result { + let i = index_arg("remove", index)?; + if i >= items.len() { + return Err(InterpreterError::RuntimeError(format!( + "remove: index {i} out of range for array of length {}", + items.len() + ))); + } + Ok(items.remove(i)) +} + +/// # Complexity +/// Cyclomatic complexity: 2 +fn insert_at(items: &mut Vec, index: &Value, item: &Value) -> Result<(), InterpreterError> { + let i = index_arg("insert", index)?; + if i > items.len() { + return Err(InterpreterError::RuntimeError(format!( + "insert: index {i} out of range for array of length {}", + items.len() + ))); + } + items.insert(i, item.clone()); + Ok(()) +} + +/// The elements an `append`/`extend` argument contributes: an array's or a +/// tuple's elements, or the integers of an integer range. +/// +/// # Complexity +/// Cyclomatic complexity: 4 +fn iterable_items(method: &str, value: &Value) -> Result, InterpreterError> { + match value { + Value::Array(arr) => Ok(arr.to_vec()), + Value::Tuple(elems) => Ok(elems.to_vec()), + Value::Range { + start, + end, + inclusive, + } => range_items(method, start, end, *inclusive), + other => Err(InterpreterError::RuntimeError(format!( + "{method}: expected an array, got {}", + other.type_name() + ))), + } +} + +/// # Complexity +/// Cyclomatic complexity: 3 +fn range_items( + method: &str, + start: &Value, + end: &Value, + inclusive: bool, +) -> Result, InterpreterError> { + let (Value::Integer(lo), Value::Integer(hi)) = (start, end) else { + return Err(InterpreterError::RuntimeError(format!( + "{method}: expected an integer range" + ))); + }; + let values: Vec = if inclusive { + (*lo..=*hi).map(Value::Integer).collect() + } else { + (*lo..*hi).map(Value::Integer).collect() + }; + Ok(values) +} + // No-argument array methods (complexity <= 3 each) fn eval_array_len(arr: &Arc<[Value]>) -> Result { @@ -1074,3 +1231,105 @@ mod sort_order_tests { } } } + +/// ARRAYMUT-1: in-place methods change the receiver and return Rust's result. +/// `extend` is covered here because `.extend(` does not parse yet (EXTENDKW-1). +#[cfg(test)] +mod in_place_tests { + use super::{apply_in_place_array_method, is_in_place_array_method}; + use crate::runtime::Value; + use std::sync::Arc; + + fn ints(values: &[i64]) -> Vec { + values.iter().copied().map(Value::Integer).collect() + } + + fn apply(items: &mut Vec, method: &str, args: &[Value]) -> Value { + apply_in_place_array_method(items, method, args).expect("in-place method succeeds") + } + + #[test] + fn test_arraymut_1_extend_with_array_appends_elements() { + let mut items = ints(&[1, 2]); + let other = Value::Array(Arc::from(ints(&[3, 4]))); + assert_eq!(apply(&mut items, "extend", &[other]), Value::Nil); + assert_eq!(items, ints(&[1, 2, 3, 4])); + } + + #[test] + fn test_arraymut_1_extend_with_ranges_appends_integers() { + let mut items = ints(&[0]); + apply( + &mut items, + "extend", + &[Value::from_range( + Value::Integer(1), + Value::Integer(3), + false, + )], + ); + apply( + &mut items, + "extend", + &[Value::from_range( + Value::Integer(7), + Value::Integer(8), + true, + )], + ); + assert_eq!(items, ints(&[0, 1, 2, 7, 8])); + } + + #[test] + fn test_arraymut_1_extend_with_tuple_appends_elements() { + let mut items = Vec::new(); + let tuple = Value::Tuple(Arc::from(ints(&[5, 6]))); + apply(&mut items, "extend", &[tuple]); + assert_eq!(items, ints(&[5, 6])); + } + + #[test] + fn test_arraymut_1_extend_rejects_non_iterable() { + let mut items = ints(&[1]); + let err = apply_in_place_array_method(&mut items, "extend", &[Value::Integer(3)]); + assert!(err.is_err()); + assert_eq!(items, ints(&[1])); + } + + #[test] + fn test_arraymut_1_pop_and_push_keep_their_results() { + let mut items = ints(&[1]); + assert_eq!(apply(&mut items, "push", &[Value::Integer(2)]), Value::Nil); + assert_eq!(apply(&mut items, "pop", &[]), Value::Integer(2)); + assert_eq!(apply(&mut items, "pop", &[]), Value::Integer(1)); + assert_eq!(apply(&mut items, "pop", &[]), Value::Nil); + } + + #[test] + fn test_arraymut_1_bad_indices_are_errors() { + let mut items = ints(&[1, 2]); + let bad = [ + ("remove", vec![Value::Integer(2)]), + ("remove", vec![Value::Integer(-1)]), + ("insert", vec![Value::Integer(3), Value::Integer(0)]), + ("truncate", vec![Value::from_string("x".to_string())]), + ]; + for (method, args) in bad { + assert!( + apply_in_place_array_method(&mut items, method, &args).is_err(), + "{method}" + ); + } + assert_eq!(items, ints(&[1, 2])); + } + + #[test] + fn test_arraymut_1_table_matches_arity() { + assert!(is_in_place_array_method("insert", 2)); + assert!(!is_in_place_array_method("insert", 1)); + assert!(is_in_place_array_method("sort", 0)); + assert!(!is_in_place_array_method("sort", 1)); + assert!(!is_in_place_array_method("sorted", 0)); + assert!(!is_in_place_array_method("reversed", 0)); + } +} diff --git a/src/runtime/interpreter_methods_dispatch.rs b/src/runtime/interpreter_methods_dispatch.rs index 495ddf630..b65661ab3 100644 --- a/src/runtime/interpreter_methods_dispatch.rs +++ b/src/runtime/interpreter_methods_dispatch.rs @@ -15,6 +15,8 @@ use crate::runtime::interpreter::Interpreter; use crate::runtime::{InterpreterError, Value}; use std::sync::Arc; +use crate::runtime::eval_array::{apply_in_place_array_method, is_in_place_array_method}; + /// Which handler an object's dispatch marker selects, checked in the order /// actor, class, struct, `__type`. enum ObjectKind { @@ -45,17 +47,11 @@ impl ObjectKind { } } -/// FIELDPOP-1: array methods that change their receiver in place — the same -/// set the interpreter mutates for a local array receiver (`a.push(x)`, `a.pop()`). -fn is_mutating_array_call(method: &str, arg_count: usize) -> bool { - matches!((method, arg_count), ("push", 1) | ("pop", 0)) -} - impl Interpreter { - /// FIELDPOP-1: `obj.field.push(x)` / `obj.field.pop()` on an array field - /// behave like the same call on a local array: the call returns the - /// method's own result (`pop` → the removed element, `nil` when empty; - /// `push` → `nil`) and the changed array is written back to the field. + /// FIELDPOP-1 / ARRAYMUT-1: an in-place array method on a field + /// (`self.items.pop()`, `s.items.sort()`, `o.items.insert(0, x)`) behaves + /// like the same call on a local array: the call returns the method's own + /// result and the changed array is written back to the field. /// /// Returns `Ok(None)` when the call is not of that shape, so the caller /// falls through to ordinary method dispatch. @@ -69,24 +65,59 @@ impl Interpreter { return Ok(None); }; let rooted = matches!(object.kind, ExprKind::Identifier(_)); - if !rooted || !is_mutating_array_call(method, args.len()) { + if !rooted || !is_in_place_array_method(method, args.len()) { return Ok(None); } let Value::Array(arr) = self.eval_expr(receiver)? else { return Ok(None); }; - let mut items = arr.to_vec(); - let result = match args.first() { - Some(arg) => { - items.push(self.eval_expr(arg)?); - Value::Nil - } - None => items.pop().unwrap_or(Value::Nil), - }; + let (items, result) = self.apply_in_place_call(&arr, method, args)?; self.eval_field_assign(object, field, Value::Array(Arc::from(items)))?; Ok(Some(result)) } + /// ARRAYMUT-1: an in-place array method on a local array variable + /// (`v.push(x)`, `v.pop()`, `v.sort()`, `v.remove(i)`, …) returns the + /// method's own result and rebinds the variable to the changed array. + /// `env_set_mut` updates the binding in an enclosing scope too, so the + /// change survives a call made inside a loop body. + /// + /// Returns `Ok(None)` when the receiver is not an array variable or the + /// method is not in-place, so the caller falls through. + fn try_local_array_mutation( + &mut self, + var_name: &str, + method: &str, + args: &[Expr], + ) -> Result, InterpreterError> { + if !is_in_place_array_method(method, args.len()) { + return Ok(None); + } + let Ok(Value::Array(arr)) = self.lookup_variable(var_name) else { + return Ok(None); + }; + let (items, result) = self.apply_in_place_call(&arr, method, args)?; + self.env_set_mut(var_name.to_string(), Value::Array(Arc::from(items))); + Ok(Some(result)) + } + + /// Evaluate `args`, then apply the in-place `method` to a copy of `arr`. + /// Returns the changed elements and the method's result. + fn apply_in_place_call( + &mut self, + arr: &[Value], + method: &str, + args: &[Expr], + ) -> Result<(Vec, Value), InterpreterError> { + let arg_values = args + .iter() + .map(|arg| self.eval_expr(arg)) + .collect::, _>>()?; + let mut items = arr.to_vec(); + let result = apply_in_place_array_method(&mut items, method, &arg_values)?; + Ok((items, result)) + } + /// Evaluate a method call pub(crate) fn eval_method_call( &mut self, @@ -101,38 +132,11 @@ impl Interpreter { } } - // Special handling for mutating array methods on simple identifiers - // e.g., messages.push(item) + // ARRAYMUT-1: in-place array methods on a local array variable + // (messages.push(item), v.sort(), v.remove(0)) mutate the binding if let ExprKind::Identifier(var_name) = &receiver.kind { - if method == "push" && args.len() == 1 { - // Get current array value - if let Ok(Value::Array(arr)) = self.lookup_variable(var_name) { - // Evaluate the argument - let arg_value = self.eval_expr(&args[0])?; - - // Create new array with item added - let mut new_arr = arr.to_vec(); - new_arr.push(arg_value); - - // Update the variable binding - CRITICAL: Use env_set_mut to update - // in parent scopes (e.g., when push is called inside while loops) - self.env_set_mut(var_name.clone(), Value::Array(Arc::from(new_arr))); - - return Ok(Value::Nil); // push returns nil - } - } else if method == "pop" && args.is_empty() { - // Get current array value - if let Ok(Value::Array(arr)) = self.lookup_variable(var_name) { - // Create new array with last item removed - let mut new_arr = arr.to_vec(); - let popped_value = new_arr.pop().unwrap_or(Value::Nil); - - // Update the variable binding - CRITICAL: Use env_set_mut to update - // in parent scopes (e.g., when pop is called inside while loops) - self.env_set_mut(var_name.clone(), Value::Array(Arc::from(new_arr))); - - return Ok(popped_value); // pop returns the removed item - } + if let Some(result) = self.try_local_array_mutation(var_name, method, args)? { + return Ok(result); } } From bd6cbbf0716984d959a8864b78ba1de96baa90f2 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:51:20 +0200 Subject: [PATCH 048/106] EXTENDKW-1 (RED): keyword tokens as method/field names after '.' - tests/extendkw_1_keyword_methods.rs: v.extend(x), every keyword as a method and field name (check + transpile), r#-escaped reserved method names, keyword method chains, ?. members, x.await unchanged, tuple index unchanged, rustc-compiled b.items.extend([3]) - RED on HEAD: 'Expected method name, tuple index, or await after .' Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 4896 ++++++++++++++++----------- tests/extendkw_1_keyword_methods.rs | 243 ++ 2 files changed, 3106 insertions(+), 2033 deletions(-) create mode 100644 tests/extendkw_1_keyword_methods.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 61648ed1a..c3883e0fc 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-23T14:08:18.278349727Z", + "created_at": "2026-09-24T05:58:21.272608795Z", "git_context": null, "files": { "./build.rs": { @@ -1334,48 +1334,48 @@ }, "./ruchy-embed/src/lib.rs": { "content_hash": [ - 182, + 191, + 153, + 206, + 223, + 218, 172, - 9, - 144, - 64, - 168, - 166, - 252, - 142, - 215, - 221, - 36, - 161, - 204, + 23, 97, - 160, - 119, - 118, - 121, - 78, - 169, - 166, - 237, - 148, - 212, - 22, - 252, - 240, + 25, + 67, + 114, + 162, + 203, + 157, + 5, + 149, 54, - 46, - 174, - 133 + 161, + 3, + 242, + 62, + 176, + 237, + 73, + 151, + 70, + 34, + 93, + 38, + 214, + 74, + 153 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.336245, + "duplication_ratio": 17.391304, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.03295, + "total": 93.08301, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -1391,11 +1391,11 @@ }, { "source_metric": "Duplication", - "amount": 2.6637554, + "amount": 2.6086955, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.3%" + "issue": "Code duplication: 13.0%" } ], "critical_defects_count": 0, @@ -2432,48 +2432,48 @@ }, "./src/backend/compiler.rs": { "content_hash": [ - 47, - 42, - 157, - 120, - 86, - 178, - 224, - 81, - 231, - 68, - 145, - 202, - 135, - 30, - 169, - 48, - 38, - 112, - 137, - 21, - 147, - 96, - 83, - 203, - 80, - 19, + 251, + 110, 208, - 96, - 128, - 119, - 59, - 90 + 229, + 212, + 140, + 236, + 209, + 210, + 53, + 103, + 199, + 13, + 25, + 21, + 197, + 243, + 150, + 195, + 68, + 225, + 55, + 148, + 67, + 75, + 166, + 68, + 141, + 43, + 205, + 8, + 116 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.601307, + "duplication_ratio": 16.494623, "coupling_score": 14.6, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.20131, + "total": 76.09462, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -2485,15 +2485,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 81 duplicate code patterns" + "issue": "Found 84 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.3986928, + "amount": 3.5053763, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 17.0%" + "issue": "Code duplication: 17.5%" }, { "source_metric": "StructuralComplexity", @@ -3238,38 +3238,38 @@ }, "./src/backend/transpiler/block_transpiler.rs": { "content_hash": [ - 251, - 149, - 52, - 146, - 131, - 122, - 31, - 249, - 46, - 124, - 121, - 167, - 172, - 185, - 96, - 35, - 82, - 34, - 185, - 186, - 160, - 192, - 56, - 49, + 24, + 53, + 7, + 16, + 134, 199, + 234, + 124, + 168, + 127, + 238, + 234, + 95, + 42, + 7, + 97, + 139, + 64, + 218, + 36, 165, - 118, - 254, + 163, + 172, + 197, + 153, + 180, + 10, 67, - 22, - 61, - 4 + 156, + 55, + 206, + 157 ], "score": { "structural_complexity": 23.8, @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 197, - 159, - 185, - 63, - 204, - 21, - 95, - 14, - 229, - 178, - 11, - 42, + 96, 232, - 65, - 23, - 130, - 204, - 52, - 101, - 12, - 81, - 204, - 147, - 10, - 22, - 167, - 110, - 7, - 7, - 219, - 109, - 146 + 11, + 3, + 248, + 235, + 105, + 166, + 149, + 163, + 45, + 151, + 174, + 153, + 132, + 127, + 185, + 51, + 76, + 15, + 61, + 131, + 191, + 97, + 40, + 0, + 78, + 35, + 83, + 199, + 222, + 86 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.546772, + "duplication_ratio": 15.912939, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.54677, + "total": 75.91294, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3639,15 +3639,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 65 duplicate code patterns" + "issue": "Found 68 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.453228, + "amount": 4.0870614, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 22.3%" + "issue": "Code duplication: 20.4%" }, { "source_metric": "StructuralComplexity", @@ -3655,15 +3655,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 64" + "issue": "High cognitive complexity: 68" }, { "source_metric": "StructuralComplexity", - "amount": 14.0, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 58" + "issue": "High cyclomatic complexity: 68" } ], "critical_defects_count": 0, @@ -4464,48 +4464,48 @@ }, "./src/backend/transpiler/dispatcher.rs": { "content_hash": [ - 241, - 191, - 130, - 216, - 228, - 0, - 125, - 81, - 206, - 108, + 24, + 186, + 56, + 230, + 18, + 246, + 217, 68, - 121, - 138, - 16, - 137, - 139, - 243, - 42, - 223, + 133, + 169, + 49, + 47, + 175, + 99, + 239, + 146, + 161, 251, - 229, - 187, - 119, - 66, - 46, - 166, - 113, + 49, + 16, + 202, + 15, + 120, + 145, 200, - 219, - 152, - 182, - 106 + 154, + 74, + 133, + 105, + 106, + 0, + 179 ], "score": { "structural_complexity": 6.1, "semantic_complexity": 20.0, - "duplication_ratio": 12.917809, + "duplication_ratio": 12.927496, "coupling_score": 14.8, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 78.81781, + "total": 78.8275, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -4521,7 +4521,7 @@ }, { "source_metric": "Duplication", - "amount": 7.0821915, + "amount": 7.0725036, "applied_to": [ "Duplication" ], @@ -5143,48 +5143,48 @@ }, "./src/backend/transpiler/expr_dispatcher.rs": { "content_hash": [ - 214, - 14, - 169, - 91, - 253, - 30, + 61, + 94, + 193, + 163, + 6, + 58, + 93, 181, - 43, - 39, - 158, - 149, - 208, - 64, - 141, - 102, - 233, - 32, - 107, - 63, - 9, - 25, - 78, - 24, - 89, - 11, - 82, - 108, - 182, - 4, - 253, - 241, - 1 + 111, + 156, + 133, + 177, + 211, + 1, + 240, + 100, + 119, + 251, + 237, + 91, + 96, + 196, + 20, + 220, + 234, + 66, + 169, + 56, + 168, + 116, + 242, + 72 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.828571, + "duplication_ratio": 17.877094, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.480515, + "total": 93.52463, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -5200,11 +5200,11 @@ }, { "source_metric": "Duplication", - "amount": 2.1714284, + "amount": 2.122905, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 10.9%" + "issue": "Code duplication: 10.6%" } ], "critical_defects_count": 0, @@ -5317,38 +5317,38 @@ }, "./src/backend/transpiler/expressions.rs": { "content_hash": [ - 88, - 174, - 107, - 225, + 90, + 75, + 96, + 171, + 202, + 232, + 119, + 123, + 162, 59, - 220, - 250, - 19, - 170, - 7, - 136, - 103, - 158, - 53, - 102, - 152, - 76, - 243, - 34, - 47, - 12, - 39, - 184, - 73, - 220, - 222, - 7, + 125, + 248, + 37, + 116, + 65, + 225, 184, - 237, - 172, - 40, - 150 + 228, + 221, + 174, + 167, + 174, + 8, + 0, + 206, + 81, + 12, + 245, + 158, + 29, + 28, + 107 ], "score": { "structural_complexity": 0.0, @@ -5570,48 +5570,48 @@ }, "./src/backend/transpiler/expressions_helpers/binary_ops.rs": { "content_hash": [ - 87, - 7, - 145, - 52, - 194, - 48, - 112, - 236, - 241, - 26, - 133, - 27, - 54, - 186, - 149, - 21, - 122, - 38, - 30, - 234, - 66, - 123, + 239, + 80, 182, - 159, - 208, + 245, + 227, + 243, + 160, + 204, + 196, + 191, + 110, + 213, + 163, + 36, + 119, + 107, + 144, 203, - 129, - 246, - 121, - 85, - 210, - 180 + 130, + 108, + 68, + 33, + 139, + 168, + 141, + 242, + 43, + 158, + 220, + 107, + 244, + 181 ], "score": { "structural_complexity": 4.6, "semantic_complexity": 20.0, - "duplication_ratio": 16.93166, + "duplication_ratio": 16.989246, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 81.53166, + "total": 81.58925, "grade": "B+", "confidence": 1.0, "language": "Rust", @@ -5623,15 +5623,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 42 duplicate code patterns" + "issue": "Found 44 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.0683403, + "amount": 3.0107527, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.3%" + "issue": "Code duplication: 15.1%" }, { "source_metric": "StructuralComplexity", @@ -5665,48 +5665,48 @@ }, "./src/backend/transpiler/expressions_helpers/collections.rs": { "content_hash": [ - 236, - 209, - 63, - 81, - 30, - 35, - 151, - 150, - 10, - 139, - 175, - 7, - 62, - 137, - 47, - 231, - 179, - 63, + 51, + 111, + 72, + 184, + 51, + 77, + 188, 94, - 132, - 124, - 23, - 248, - 195, - 179, - 22, - 255, - 49, - 122, + 55, + 114, + 85, + 245, + 71, + 177, + 230, + 31, + 241, 194, - 198, - 215 + 64, + 213, + 149, + 235, + 245, + 19, + 103, + 139, + 100, + 29, + 126, + 119, + 248, + 173 ], "score": { - "structural_complexity": 16.9, + "structural_complexity": 16.599998, "semantic_complexity": 20.0, - "duplication_ratio": 12.539007, + "duplication_ratio": 12.714682, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 89.43901, + "total": 89.31468, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -5718,23 +5718,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 73 duplicate code patterns" + "issue": "Found 72 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 7.460993, + "amount": 7.2853184, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 37.3%" + "issue": "Code duplication: 36.4%" }, { "source_metric": "StructuralComplexity", - "amount": 8.1, + "amount": 8.400001, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 42" + "issue": "High cognitive complexity: 43" } ], "critical_defects_count": 0, @@ -5752,48 +5752,48 @@ }, "./src/backend/transpiler/expressions_helpers/field_access.rs": { "content_hash": [ - 35, - 208, - 60, - 158, - 246, + 12, + 216, + 108, + 34, + 212, 199, - 116, - 174, - 21, - 205, - 156, - 154, - 122, - 190, - 60, - 226, - 152, + 54, + 94, + 220, + 247, + 184, + 248, + 2, 78, - 182, - 158, + 175, + 208, + 27, + 254, + 121, + 214, + 154, + 0, + 31, 102, - 184, - 239, - 196, - 3, - 232, - 4, - 67, - 42, - 171, - 65, - 251 + 108, + 37, + 235, + 55, + 18, + 203, + 150, + 25 ], "score": { - "structural_complexity": 17.1, + "structural_complexity": 16.3, "semantic_complexity": 20.0, - "duplication_ratio": 14.446131, + "duplication_ratio": 14.467456, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 91.54613, + "total": 90.767456, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -5813,31 +5813,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 49 duplicate code patterns" + "issue": "Found 52 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.5538697, + "amount": 5.532544, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 27.8%" + "issue": "Code duplication: 27.7%" }, { "source_metric": "StructuralComplexity", - "amount": 5.4, + "amount": 5.7000003, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 33" + "issue": "High cognitive complexity: 34" }, { "source_metric": "StructuralComplexity", - "amount": 0.5, + "amount": 1.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 31" + "issue": "High cyclomatic complexity: 32" } ], "critical_defects_count": 0, @@ -6091,48 +6091,48 @@ }, "./src/backend/transpiler/function_signature.rs": { "content_hash": [ - 216, - 12, - 223, - 11, + 217, + 87, + 242, + 178, + 187, + 154, + 105, + 134, + 100, + 39, + 8, + 245, + 119, + 59, + 71, + 105, + 179, + 87, + 167, + 158, + 186, + 189, + 85, + 231, + 237, + 227, + 87, + 74, 159, - 233, - 88, - 246, - 253, - 215, - 33, - 95, - 152, - 73, - 218, - 116, - 72, - 94, - 5, - 169, - 79, - 75, - 147, - 46, - 48, - 174, - 222, - 141, - 19, - 137, - 160, - 213 + 200, + 165, + 140 ], "score": { - "structural_complexity": 0.09999943, + "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.774971, + "duplication_ratio": 16.813318, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.87497, + "total": 76.81332, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -6156,27 +6156,27 @@ }, { "source_metric": "Duplication", - "amount": 3.22503, + "amount": 3.1866825, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 16.1%" + "issue": "Code duplication: 15.9%" }, { "source_metric": "StructuralComplexity", - "amount": 9.900001, + "amount": 10.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 48" + "issue": "High cognitive complexity: 50" }, { "source_metric": "StructuralComplexity", - "amount": 14.0, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 58" + "issue": "High cyclomatic complexity: 60" } ], "critical_defects_count": 0, @@ -6194,48 +6194,48 @@ }, "./src/backend/transpiler/function_transpiler.rs": { "content_hash": [ - 176, - 103, - 190, - 94, - 78, - 249, - 64, - 137, - 21, + 156, + 221, + 122, 247, - 177, - 61, - 254, - 26, - 41, - 179, - 15, - 142, - 147, - 165, - 68, - 57, - 254, - 41, - 42, + 38, + 43, + 187, + 47, + 150, + 58, + 39, + 157, + 169, + 245, + 46, + 209, + 60, + 203, + 64, + 193, + 64, + 53, + 238, + 117, + 5, 253, - 4, - 78, - 112, - 40, - 40, - 157 + 249, + 201, + 110, + 101, + 240, + 5 ], "score": { - "structural_complexity": 19.6, + "structural_complexity": 17.6, "semantic_complexity": 20.0, - "duplication_ratio": 14.026667, + "duplication_ratio": 15.228628, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.62666, + "total": 92.82863, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -6247,23 +6247,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 22 duplicate code patterns" + "issue": "Found 32 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.9733334, + "amount": 4.771372, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.9%" + "issue": "Code duplication: 23.9%" }, { "source_metric": "StructuralComplexity", - "amount": 5.4, + "amount": 6.9, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 33" + "issue": "High cognitive complexity: 38" + }, + { + "source_metric": "StructuralComplexity", + "amount": 0.5, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 31" } ], "critical_defects_count": 0, @@ -7212,49 +7220,49 @@ }, "./src/backend/transpiler/mutation_detection.rs": { "content_hash": [ + 55, + 50, + 59, + 177, 155, - 148, - 168, - 89, - 70, - 238, - 241, - 183, - 239, - 22, - 73, - 43, - 158, - 64, - 229, - 149, - 122, - 213, - 37, - 63, - 62, - 117, - 138, - 14, - 222, - 62, - 113, - 252, - 23, - 194, - 103, - 68 + 129, + 81, + 250, + 53, + 180, + 50, + 205, + 197, + 144, + 49, + 9, + 142, + 66, + 205, + 135, + 79, + 40, + 127, + 99, + 36, + 151, + 161, + 199, + 104, + 106, + 179, + 249 ], "score": { - "structural_complexity": 19.3, + "structural_complexity": 17.2, "semantic_complexity": 20.0, - "duplication_ratio": 16.551723, + "duplication_ratio": 17.072136, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 95.85172, - "grade": "A+", + "total": 94.27214, + "grade": "A", "confidence": 1.0, "language": "Rust", "file_path": "./src/backend/transpiler/mutation_detection.rs", @@ -7265,23 +7273,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 49 duplicate code patterns" + "issue": "Found 109 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.448276, + "amount": 2.927864, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 17.2%" + "issue": "Code duplication: 14.6%" }, { "source_metric": "StructuralComplexity", - "amount": 5.7000003, + "amount": 0.3, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 34" + "issue": "High cognitive complexity: 16" + }, + { + "source_metric": "StructuralComplexity", + "amount": 7.5, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 45" } ], "critical_defects_count": 0, @@ -7750,48 +7766,48 @@ }, "./src/backend/transpiler/program_transpiler.rs": { "content_hash": [ - 160, - 209, - 255, - 12, - 153, - 198, - 132, - 226, - 142, - 128, + 42, + 2, + 205, + 188, + 0, + 88, + 220, + 162, + 165, + 207, + 141, + 215, + 196, + 149, + 65, + 164, + 68, + 93, + 74, + 137, + 245, 150, - 178, - 67, - 95, - 86, - 186, - 103, - 154, - 1, - 52, - 145, - 103, - 180, - 212, - 151, - 26, - 192, + 64, + 5, + 215, + 249, 68, - 223, - 80, - 38, - 223 + 212, + 208, + 15, + 12, + 2 ], "score": { - "structural_complexity": 9.0, + "structural_complexity": 8.4, "semantic_complexity": 20.0, - "duplication_ratio": 12.835052, + "duplication_ratio": 12.920204, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 81.83505, + "total": 81.320206, "grade": "B+", "confidence": 1.0, "language": "Rust", @@ -7803,31 +7819,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 122 duplicate code patterns" + "issue": "Found 124 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 7.1649485, + "amount": 7.0797963, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 35.8%" + "issue": "Code duplication: 35.4%" }, { "source_metric": "StructuralComplexity", - "amount": 7.5000005, + "amount": 6.6000004, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 40" + "issue": "High cognitive complexity: 37" }, { "source_metric": "StructuralComplexity", - "amount": 8.5, + "amount": 10.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 47" + "issue": "High cyclomatic complexity: 50" } ], "critical_defects_count": 0, @@ -7924,38 +7940,38 @@ }, "./src/backend/transpiler/return_type_helpers.rs": { "content_hash": [ - 145, - 175, - 214, - 40, + 255, + 92, + 236, + 189, + 185, + 193, + 156, 143, - 55, - 61, - 180, - 47, - 69, - 184, - 24, - 53, - 214, - 244, - 88, - 58, - 132, - 119, - 237, - 51, - 26, 118, - 27, + 110, + 58, + 206, + 109, + 200, + 104, + 20, + 207, + 240, + 60, + 93, 8, - 45, + 237, + 38, + 54, 109, - 29, - 40, - 107, - 185, - 31 + 188, + 63, + 128, + 9, + 197, + 101, + 119 ], "score": { "structural_complexity": 0.0, @@ -8802,48 +8818,48 @@ }, "./src/backend/transpiler/types.rs": { "content_hash": [ - 217, - 10, + 236, 232, - 214, - 75, - 78, - 9, - 111, - 254, - 146, - 105, - 170, - 154, - 239, + 220, 103, - 186, - 48, + 69, + 244, + 15, + 237, + 89, + 169, + 21, + 161, + 252, + 119, + 122, + 100, + 183, + 25, + 155, + 154, + 111, + 27, + 228, + 178, + 249, 30, - 232, - 143, - 68, - 66, - 127, - 159, - 118, - 243, - 94, - 140, - 8, - 104, - 153, - 248 + 229, + 52, + 43, + 38, + 231, + 100 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.392405, + "duplication_ratio": 15.679611, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.3924, + "total": 75.67961, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -8863,15 +8879,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 137 duplicate code patterns" + "issue": "Found 133 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.607595, + "amount": 4.3203883, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 23.0%" + "issue": "Code duplication: 21.6%" }, { "source_metric": "StructuralComplexity", @@ -8879,7 +8895,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 157" + "issue": "High cognitive complexity: 152" }, { "source_metric": "StructuralComplexity", @@ -8887,7 +8903,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 130" + "issue": "High cyclomatic complexity: 135" } ], "critical_defects_count": 0, @@ -8905,38 +8921,38 @@ }, "./src/backend/transpiler/utility_builtins.rs": { "content_hash": [ - 125, - 165, - 17, - 234, - 11, - 21, - 90, - 84, - 24, - 152, - 188, - 114, - 111, - 53, - 221, - 96, - 146, - 158, - 167, - 215, - 24, - 211, + 37, + 184, + 142, + 47, + 39, + 46, + 32, + 18, + 238, + 201, + 77, + 87, 13, - 193, - 8, - 245, - 188, - 108, + 173, + 205, + 33, + 210, 174, - 247, - 92, - 236 + 28, + 240, + 161, + 239, + 83, + 93, + 176, + 235, + 82, + 103, + 35, + 185, + 185, + 10 ], "score": { "structural_complexity": 23.8, @@ -9711,38 +9727,38 @@ }, "./src/bench/mod.rs": { "content_hash": [ - 105, - 149, - 47, - 133, - 138, - 84, - 106, - 214, - 115, - 148, - 164, - 65, - 87, - 124, - 101, - 198, - 165, - 17, - 147, + 192, + 56, + 75, + 33, + 13, + 125, + 0, + 241, 121, - 163, - 51, - 52, - 203, - 94, - 123, - 134, - 1, - 106, - 143, + 241, + 63, + 132, + 190, + 154, 62, - 174 + 102, + 203, + 169, + 126, + 149, + 250, + 77, + 12, + 213, + 224, + 77, + 64, + 145, + 112, + 68, + 101, + 45 ], "score": { "structural_complexity": 25.0, @@ -10454,48 +10470,48 @@ }, "./src/bin/handlers/command_router.rs": { "content_hash": [ - 188, - 7, - 223, - 224, - 33, - 164, - 2, - 91, - 61, - 174, - 99, - 138, - 253, - 4, - 18, - 96, - 62, - 69, - 79, - 64, - 198, - 240, - 75, - 239, - 52, - 57, - 245, - 219, - 209, - 98, + 236, + 103, + 241, + 104, + 236, + 159, + 80, + 199, + 187, + 0, + 135, + 148, + 39, + 175, + 162, + 186, + 53, + 155, + 213, + 81, + 53, 154, - 153 + 249, + 149, + 28, + 162, + 196, + 204, + 163, + 253, + 223, + 180 ], "score": { "structural_complexity": 8.2, "semantic_complexity": 20.0, - "duplication_ratio": 17.793493, + "duplication_ratio": 17.797695, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 85.99349, + "total": 85.997696, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -10507,11 +10523,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 121 duplicate code patterns" + "issue": "Found 130 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.2065065, + "amount": 2.2023048, "applied_to": [ "Duplication" ], @@ -10531,7 +10547,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 66" + "issue": "High cyclomatic complexity: 67" } ], "critical_defects_count": 0, @@ -11727,49 +11743,49 @@ }, "./src/bin/handlers/handlers_modules/sovereign.rs": { "content_hash": [ - 112, - 104, - 249, - 213, - 99, - 193, - 94, - 172, - 199, - 27, - 118, + 244, + 107, + 68, + 82, + 187, + 169, + 253, + 106, + 175, + 41, + 96, + 42, + 49, + 46, + 11, + 187, + 91, 25, - 131, - 75, - 71, - 178, - 199, - 134, - 59, - 57, - 93, - 190, + 47, + 160, + 225, + 41, + 189, + 91, + 61, + 161, + 63, + 193, 84, - 212, - 50, - 235, - 85, - 48, - 3, - 133, + 206, 70, - 42 + 86 ], "score": { - "structural_complexity": 3.0, + "structural_complexity": 2.5, "semantic_complexity": 20.0, - "duplication_ratio": 17.069923, + "duplication_ratio": 17.13355, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 80.06992, - "grade": "B+", + "total": 79.63355, + "grade": "B", "confidence": 1.0, "language": "Rust", "file_path": "./src/bin/handlers/handlers_modules/sovereign.rs", @@ -11784,11 +11800,11 @@ }, { "source_metric": "Duplication", - "amount": 2.9300776, + "amount": 2.8664496, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 14.7%" + "issue": "Code duplication: 14.3%" }, { "source_metric": "StructuralComplexity", @@ -11796,15 +11812,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 62" + "issue": "High cognitive complexity: 63" }, { "source_metric": "StructuralComplexity", - "amount": 12.0, + "amount": 12.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 54" + "issue": "High cyclomatic complexity: 55" } ], "critical_defects_count": 0, @@ -11901,48 +11917,48 @@ }, "./src/bin/handlers/mcp_handler.rs": { "content_hash": [ - 204, - 28, - 67, - 219, - 46, - 151, - 221, - 126, - 78, - 198, - 241, - 92, - 37, - 140, + 35, + 53, + 27, + 13, + 127, + 235, + 244, 172, - 86, - 122, - 229, - 38, - 241, - 31, + 5, 60, - 179, - 143, - 100, - 237, - 124, + 206, + 134, + 18, + 190, + 185, + 27, + 175, + 141, + 183, + 82, + 145, + 63, + 85, + 153, + 45, + 111, + 154, + 17, + 32, 140, - 214, - 61, - 177, - 91 + 150, + 245 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 19.0, - "duplication_ratio": 20.0, + "duplication_ratio": 16.774193, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 94.545456, + "total": 91.61291, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -11954,7 +11970,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 19 duplicate code patterns" + "issue": "Found 14 duplicate code patterns" + }, + { + "source_metric": "Duplication", + "amount": 3.2258062, + "applied_to": [ + "Duplication" + ], + "issue": "Code duplication: 16.1%" }, { "source_metric": "SemanticComplexity", @@ -12897,38 +12921,38 @@ }, "./src/bin/handlers/rhl_handler.rs": { "content_hash": [ - 31, - 177, - 224, - 170, - 148, - 1, - 43, - 252, - 160, + 44, + 34, + 166, + 122, + 81, 28, - 87, - 217, - 220, - 140, - 144, - 219, - 2, - 183, - 250, - 141, - 82, + 213, + 251, + 166, + 202, 50, - 71, - 188, - 124, - 235, - 45, - 159, - 105, - 58, + 182, + 158, + 35, + 239, + 34, + 16, 111, - 83 + 113, + 95, + 179, + 145, + 7, + 189, + 38, + 82, + 116, + 232, + 174, + 168, + 10, + 140 ], "score": { "structural_complexity": 25.0, @@ -12937,8 +12961,8 @@ "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, - "entropy_score": 8.5, - "total": 98.63637, + "entropy_score": 7.0, + "total": 97.27273, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -12946,11 +12970,11 @@ "penalties_applied": [ { "source_metric": "Duplication", - "amount": 1.5, + "amount": 3.0, "applied_to": [ "Duplication" ], - "issue": "Found 3 duplicate code patterns" + "issue": "Found 6 duplicate code patterns" } ], "critical_defects_count": 0, @@ -13047,48 +13071,48 @@ }, "./src/bin/handlers/serve_handler.rs": { "content_hash": [ - 59, - 166, - 176, - 38, - 125, - 73, - 174, - 173, + 130, + 153, + 243, + 118, + 143, 132, - 25, - 135, - 55, - 200, - 136, - 205, - 241, + 70, + 138, + 217, 111, - 23, - 235, - 186, - 2, - 44, - 113, + 222, + 106, + 142, + 137, + 153, + 83, + 84, 81, - 226, - 147, - 252, - 44, - 146, - 149, - 73, - 99 + 106, + 208, + 94, + 23, + 62, + 250, + 241, + 209, + 154, + 187, + 7, + 35, + 51, + 123 ], "score": { - "structural_complexity": 19.9, + "structural_complexity": 19.0, "semantic_complexity": 17.5, - "duplication_ratio": 16.007393, + "duplication_ratio": 15.29132, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 93.407394, + "total": 91.79132, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -13100,23 +13124,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 70 duplicate code patterns" + "issue": "Found 99 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.9926064, + "amount": 4.70868, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.0%" + "issue": "Code duplication: 23.5%" }, { "source_metric": "StructuralComplexity", - "amount": 5.1000004, + "amount": 6.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 32" + "issue": "High cognitive complexity: 35" }, { "source_metric": "SemanticComplexity", @@ -13482,48 +13506,48 @@ }, "./src/bin/ruchy.rs": { "content_hash": [ - 243, - 156, - 49, - 252, - 14, - 254, - 123, - 238, - 150, + 89, + 161, + 63, + 111, + 200, 58, - 192, - 97, - 197, - 64, - 171, - 3, - 91, - 2, - 80, - 178, - 223, - 235, + 172, + 166, + 210, + 141, + 242, + 228, + 51, + 40, + 49, 45, - 74, - 108, - 131, - 27, - 215, - 64, - 35, - 93, - 161 + 120, + 65, + 32, + 37, + 209, + 143, + 214, + 209, + 15, + 147, + 85, + 233, + 21, + 1, + 149, + 239 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 18.0, - "duplication_ratio": 14.036585, + "duplication_ratio": 14.078947, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 72.03658, + "total": 72.07895, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -13535,15 +13559,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 168 duplicate code patterns" + "issue": "Found 173 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.963415, + "amount": 5.921053, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.8%" + "issue": "Code duplication: 29.6%" }, { "source_metric": "StructuralComplexity", @@ -13551,7 +13575,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 78" + "issue": "High cognitive complexity: 80" }, { "source_metric": "StructuralComplexity", @@ -13559,7 +13583,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 101" + "issue": "High cyclomatic complexity: 106" }, { "source_metric": "SemanticComplexity", @@ -14629,48 +14653,48 @@ }, "./src/frontend/ast.rs": { "content_hash": [ - 76, - 135, - 195, - 247, - 86, - 171, + 113, + 208, + 17, + 152, + 87, + 16, + 131, + 92, + 174, + 59, + 193, + 156, + 56, + 252, + 52, + 220, + 183, + 9, + 240, + 55, + 199, + 203, + 156, + 243, + 169, + 109, + 183, + 219, + 62, + 78, 33, - 130, - 153, - 29, - 244, - 186, - 207, - 108, - 53, - 138, - 223, - 196, - 46, - 127, - 10, - 69, - 147, - 157, - 228, - 229, - 127, - 57, - 96, - 191, - 135, - 243 + 204 ], "score": { "structural_complexity": 12.5, "semantic_complexity": 20.0, - "duplication_ratio": 15.49325, + "duplication_ratio": 15.500516, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 87.99325, + "total": 88.00052, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -14686,7 +14710,7 @@ }, { "source_metric": "Duplication", - "amount": 4.5067496, + "amount": 4.499484, "applied_to": [ "Duplication" ], @@ -16771,38 +16795,38 @@ }, "./src/frontend/parser/expressions_helpers/modules.rs": { "content_hash": [ - 214, - 90, - 108, - 125, - 169, - 95, - 18, - 74, - 31, - 98, - 251, - 59, - 230, - 67, - 131, - 52, - 130, - 202, - 29, - 209, - 68, - 64, - 42, - 92, - 119, - 130, - 122, - 28, - 16, - 131, - 61, - 117 + 101, + 155, + 19, + 223, + 83, + 45, + 196, + 242, + 218, + 62, + 104, + 153, + 221, + 227, + 40, + 73, + 192, + 96, + 91, + 49, + 41, + 57, + 135, + 78, + 254, + 44, + 134, + 140, + 106, + 229, + 161, + 253 ], "score": { "structural_complexity": 25.0, @@ -16824,7 +16848,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 13 duplicate code patterns" + "issue": "Found 12 duplicate code patterns" } ], "critical_defects_count": 0, @@ -16842,48 +16866,48 @@ }, "./src/frontend/parser/expressions_helpers/patterns.rs": { "content_hash": [ - 32, - 91, - 24, - 179, + 54, + 111, + 107, + 78, + 69, + 199, + 40, + 161, + 55, 208, - 164, - 135, - 171, - 89, - 102, - 210, - 186, + 36, + 93, + 111, + 105, + 122, + 43, 68, - 186, - 44, - 108, - 217, - 246, - 228, - 137, - 174, - 119, - 203, - 206, - 223, - 9, + 79, + 43, + 178, 238, - 105, - 242, - 117, - 65, - 127 + 237, + 182, + 215, + 26, + 122, + 136, + 1, + 19, + 153, + 174, + 82 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 19.0, - "duplication_ratio": 16.032389, + "duplication_ratio": 16.0364, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.03239, + "total": 75.0364, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -16899,7 +16923,7 @@ }, { "source_metric": "Duplication", - "amount": 3.9676113, + "amount": 3.9635994, "applied_to": [ "Duplication" ], @@ -16919,7 +16943,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 171" + "issue": "High cyclomatic complexity: 172" }, { "source_metric": "SemanticComplexity", @@ -17752,48 +17776,48 @@ }, "./src/frontend/parser/functions.rs": { "content_hash": [ - 154, - 113, - 12, - 169, - 17, - 136, - 7, - 230, - 153, - 174, - 6, - 106, - 189, - 76, - 123, - 146, - 35, - 21, - 64, - 30, - 167, - 224, + 95, + 144, + 172, 186, - 75, - 247, - 173, - 208, - 6, - 102, - 154, - 123, - 17 + 131, + 244, + 161, + 180, + 212, + 137, + 122, + 1, + 128, + 217, + 4, + 253, + 10, + 231, + 137, + 124, + 246, + 254, + 145, + 94, + 168, + 3, + 51, + 57, + 26, + 31, + 186, + 250 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.78125, + "duplication_ratio": 15.897436, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.78125, + "total": 75.89744, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -17805,15 +17829,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 105 duplicate code patterns" + "issue": "Found 104 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.21875, + "amount": 4.1025643, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 21.1%" + "issue": "Code duplication: 20.5%" }, { "source_metric": "StructuralComplexity", @@ -18116,48 +18140,48 @@ }, "./src/frontend/parser/mod.rs": { "content_hash": [ - 247, - 213, - 22, - 110, - 214, - 62, - 87, - 16, - 46, - 183, - 244, - 147, - 183, - 65, - 240, - 241, - 6, - 13, - 86, - 106, - 247, + 116, + 45, + 232, + 201, + 218, + 233, + 237, + 227, + 182, + 70, 124, - 231, - 76, - 41, - 125, - 33, - 17, - 3, - 148, - 226, - 100 + 38, + 250, + 207, + 92, + 155, + 251, + 134, + 55, + 134, + 132, + 27, + 154, + 209, + 110, + 250, + 133, + 173, + 105, + 210, + 132, + 26 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.003122, + "duplication_ratio": 17.042398, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.00312, + "total": 77.0424, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -18169,15 +18193,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 68 duplicate code patterns" + "issue": "Found 67 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.9968784, + "amount": 2.957601, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.0%" + "issue": "Code duplication: 14.8%" }, { "source_metric": "StructuralComplexity", @@ -18882,38 +18906,38 @@ }, "./src/frontend/parser/utils_helpers/string_interpolation.rs": { "content_hash": [ - 197, - 187, + 177, + 26, + 145, + 48, + 114, + 26, 167, - 59, - 80, - 152, - 98, - 139, + 191, + 6, + 170, + 83, + 9, 19, - 150, - 177, - 233, - 108, - 205, - 21, - 56, - 198, - 56, - 130, - 41, - 94, - 97, - 158, - 195, - 146, - 80, - 69, + 115, + 73, + 102, 159, - 142, - 240, - 169, - 165 + 127, + 28, + 69, + 112, + 38, + 231, + 202, + 203, + 71, + 251, + 139, + 212, + 17, + 100, + 55 ], "score": { "structural_complexity": 20.3, @@ -20822,6 +20846,77 @@ }, "git_context": null }, + "./src/mcp/rhl_tools.rs": { + "content_hash": [ + 62, + 74, + 163, + 104, + 209, + 199, + 229, + 216, + 81, + 169, + 60, + 27, + 102, + 83, + 77, + 58, + 105, + 60, + 208, + 63, + 192, + 253, + 245, + 181, + 98, + 36, + 247, + 27, + 53, + 201, + 63, + 132 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 95.454544, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/mcp/rhl_tools.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 11 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/mcp/tool_discovery.rs": { "content_hash": [ 250, @@ -20911,38 +21006,38 @@ }, "./src/mcp.rs": { "content_hash": [ - 217, - 224, - 220, - 85, - 184, - 30, - 184, - 4, - 49, - 82, - 59, - 199, - 200, - 249, - 190, - 79, - 96, - 156, - 253, - 138, - 68, - 107, - 90, - 11, + 135, + 215, + 251, + 50, + 22, + 194, 19, - 253, - 137, - 3, - 169, - 75, - 151, - 43 + 5, + 131, + 166, + 84, + 66, + 47, + 140, + 46, + 172, + 154, + 66, + 86, + 252, + 150, + 24, + 179, + 78, + 49, + 36, + 251, + 217, + 103, + 78, + 191, + 119 ], "score": { "structural_complexity": 25.0, @@ -20964,7 +21059,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 36 duplicate code patterns" + "issue": "Found 34 duplicate code patterns" } ], "critical_defects_count": 0, @@ -27570,48 +27665,48 @@ }, "./src/quality/formatter.rs": { "content_hash": [ - 231, - 232, - 57, - 132, - 209, - 129, - 194, - 167, - 174, - 248, - 162, - 6, - 109, - 212, - 17, - 251, - 237, - 110, - 213, - 176, - 151, - 59, - 147, - 82, - 93, - 38, - 190, - 58, - 199, - 53, - 185, - 51 + 54, + 163, + 225, + 76, + 25, + 184, + 84, + 255, + 81, + 88, + 30, + 203, + 56, + 230, + 78, + 203, + 198, + 192, + 159, + 206, + 253, + 10, + 242, + 138, + 208, + 76, + 78, + 172, + 206, + 44, + 119, + 114 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.393238, + "duplication_ratio": 16.398754, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.393234, + "total": 76.39876, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -27631,11 +27726,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 128 duplicate code patterns" + "issue": "Found 129 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.6067626, + "amount": 3.601246, "applied_to": [ "Duplication" ], @@ -27647,7 +27742,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 140" + "issue": "High cognitive complexity: 141" }, { "source_metric": "StructuralComplexity", @@ -27655,7 +27750,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 238" + "issue": "High cyclomatic complexity: 240" } ], "critical_defects_count": 0, @@ -27673,38 +27768,38 @@ }, "./src/quality/formatter_config.rs": { "content_hash": [ - 224, - 145, - 198, - 147, - 201, - 131, - 129, - 58, - 33, - 72, - 6, - 105, - 187, - 211, - 56, - 98, - 91, - 93, - 52, - 179, - 42, - 218, + 252, 179, - 106, - 123, + 135, + 66, 2, - 81, - 163, - 210, - 142, - 244, - 95 + 52, + 175, + 172, + 130, + 156, + 160, + 5, + 189, + 83, + 150, + 62, + 149, + 30, + 246, + 127, + 255, + 54, + 121, + 141, + 232, + 214, + 74, + 205, + 104, + 152, + 38, + 38 ], "score": { "structural_complexity": 25.0, @@ -29902,38 +29997,38 @@ }, "./src/rhl/check.rs": { "content_hash": [ - 207, - 62, - 191, + 68, + 228, + 168, + 155, + 232, + 157, + 114, + 168, 87, - 206, - 124, - 52, - 230, - 81, + 233, + 1, + 56, + 46, + 58, + 31, + 178, + 39, + 205, 34, - 147, - 3, - 28, - 242, - 136, - 244, - 138, - 193, - 249, - 24, + 41, + 153, + 170, + 246, + 13, + 33, + 110, + 179, + 209, 196, - 38, - 200, - 253, - 9, - 104, - 69, - 78, - 219, - 35, - 57, - 23 + 155, + 185, + 42 ], "score": { "structural_complexity": 23.2, @@ -29955,7 +30050,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 10 duplicate code patterns" + "issue": "Found 14 duplicate code patterns" }, { "source_metric": "StructuralComplexity", @@ -29981,49 +30076,49 @@ }, "./src/rhl/cli.rs": { "content_hash": [ - 221, - 154, - 38, - 188, - 220, - 229, + 158, + 144, + 241, + 223, + 92, + 202, + 131, + 169, + 254, + 130, + 238, + 208, 139, - 110, - 31, - 207, - 54, - 220, - 148, - 45, - 51, - 137, - 145, - 48, - 240, - 78, - 150, - 140, - 227, - 120, - 232, - 26, - 166, + 218, + 88, + 118, + 204, + 179, + 253, + 73, 150, - 241, - 194, - 44, - 122 + 76, + 134, + 117, + 86, + 104, + 41, + 230, + 126, + 243, + 28, + 138 ], "score": { - "structural_complexity": 22.0, + "structural_complexity": 0.0, "semantic_complexity": 20.0, "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.72727, - "grade": "A", + "total": 80.0, + "grade": "B+", "confidence": 1.0, "language": "Rust", "file_path": "./src/rhl/cli.rs", @@ -30034,23 +30129,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 21 duplicate code patterns" + "issue": "Found 47 duplicate code patterns" }, { "source_metric": "StructuralComplexity", - "amount": 1.5, + "amount": 10.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 20" + "issue": "High cognitive complexity: 50" }, { "source_metric": "StructuralComplexity", - "amount": 1.5, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 33" + "issue": "High cyclomatic complexity: 81" } ], "critical_defects_count": 0, @@ -30068,38 +30163,38 @@ }, "./src/rhl/codes.rs": { "content_hash": [ + 35, 238, - 251, - 57, - 42, - 212, - 151, - 243, - 88, - 2, - 69, - 155, - 241, - 135, + 184, + 51, + 254, + 25, + 147, 74, - 88, + 218, + 47, + 68, + 186, 163, - 127, - 196, - 191, - 6, + 80, + 141, + 159, + 116, + 48, + 242, + 192, + 143, + 165, + 238, + 96, + 118, 7, - 36, - 33, - 237, - 42, - 218, - 255, - 111, - 107, - 198, + 144, + 164, + 250, 231, - 55 + 8, + 158 ], "score": { "structural_complexity": 25.0, @@ -30108,8 +30203,8 @@ "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, - "entropy_score": 8.5, - "total": 98.63637, + "entropy_score": 7.5, + "total": 97.72727, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -30117,11 +30212,169 @@ "penalties_applied": [ { "source_metric": "Duplication", - "amount": 1.5, + "amount": 2.5, "applied_to": [ "Duplication" ], - "issue": "Found 3 duplicate code patterns" + "issue": "Found 5 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/contract.rs": { + "content_hash": [ + 158, + 135, + 143, + 79, + 221, + 80, + 51, + 214, + 62, + 212, + 215, + 105, + 91, + 243, + 115, + 29, + 17, + 183, + 233, + 246, + 159, + 29, + 97, + 124, + 30, + 123, + 162, + 116, + 179, + 112, + 59, + 224 + ], + "score": { + "structural_complexity": 6.1, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 86.1, + "grade": "A-", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/contract.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 17 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 3.9, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 28" + }, + { + "source_metric": "StructuralComplexity", + "amount": 15.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 73" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/contract_render.rs": { + "content_hash": [ + 179, + 1, + 7, + 12, + 3, + 171, + 37, + 125, + 142, + 171, + 242, + 39, + 50, + 37, + 254, + 249, + 202, + 124, + 161, + 95, + 131, + 229, + 207, + 36, + 113, + 221, + 106, + 226, + 147, + 181, + 69, + 56 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 95.454544, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/contract_render.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 14 duplicate code patterns" } ], "critical_defects_count": 0, @@ -30208,6 +30461,180 @@ }, "git_context": null }, + "./src/rhl/examples.rs": { + "content_hash": [ + 73, + 69, + 222, + 222, + 221, + 232, + 105, + 129, + 217, + 154, + 126, + 150, + 228, + 130, + 244, + 209, + 166, + 96, + 75, + 86, + 57, + 174, + 137, + 196, + 74, + 138, + 106, + 132, + 41, + 23, + 212, + 157 + ], + "score": { + "structural_complexity": 12.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 92.0, + "grade": "A", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/examples.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 27 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 6.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 35" + }, + { + "source_metric": "StructuralComplexity", + "amount": 7.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 44" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/explain.rs": { + "content_hash": [ + 221, + 48, + 84, + 65, + 152, + 112, + 60, + 137, + 143, + 205, + 1, + 10, + 7, + 66, + 108, + 56, + 24, + 120, + 232, + 51, + 166, + 100, + 157, + 140, + 134, + 251, + 193, + 240, + 103, + 166, + 110, + 186 + ], + "score": { + "structural_complexity": 13.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 93.0, + "grade": "A", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/explain.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 15 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 3.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 25" + }, + { + "source_metric": "StructuralComplexity", + "amount": 9.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 48" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/rhl/fix.rs": { "content_hash": [ 217, @@ -30289,38 +30716,38 @@ }, "./src/rhl/fmt.rs": { "content_hash": [ - 47, - 28, - 196, - 39, - 208, - 229, - 187, - 99, - 9, - 228, - 30, - 220, - 163, - 169, - 141, - 4, - 171, - 202, - 98, - 201, + 100, + 243, + 222, + 164, + 145, + 162, + 65, + 130, 234, - 58, - 228, - 52, - 179, - 25, - 5, - 17, - 155, - 179, - 133, - 181 + 76, + 243, + 92, + 144, + 169, + 139, + 120, + 95, + 159, + 21, + 177, + 218, + 253, + 82, + 181, + 92, + 198, + 73, + 200, + 47, + 206, + 69, + 81 ], "score": { "structural_complexity": 8.1, @@ -30374,40 +30801,127 @@ }, "git_context": null }, - "./src/rhl/mod.rs": { + "./src/rhl/lower.rs": { "content_hash": [ - 23, - 15, - 19, - 160, - 211, - 32, - 74, - 201, - 109, - 242, - 195, - 158, 166, - 208, - 150, - 240, - 15, - 78, - 179, - 152, - 215, + 116, + 206, + 120, + 84, + 21, + 98, + 230, + 31, + 237, + 120, + 12, + 220, + 55, + 90, 243, - 24, - 226, + 50, + 241, + 208, + 110, + 109, + 91, + 53, + 46, + 139, + 230, + 123, + 218, + 244, + 74, + 39, + 225 + ], + "score": { + "structural_complexity": 0.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 80.0, + "grade": "B+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/lower.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 77 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 10.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 88" + }, + { + "source_metric": "StructuralComplexity", + "amount": 15.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 165" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/mod.rs": { + "content_hash": [ + 12, + 40, + 19, 87, - 81, - 129, - 124, - 23, - 165, + 218, + 63, + 156, + 173, + 151, + 60, + 47, + 43, + 119, + 182, + 34, + 237, + 237, + 108, + 186, + 204, + 89, 172, - 255 + 240, + 9, + 238, + 26, + 133, + 45, + 111, + 253, + 229, + 250 ], "score": { "structural_complexity": 25.0, @@ -30516,6 +31030,148 @@ }, "git_context": null }, + "./src/rhl/receipt.rs": { + "content_hash": [ + 120, + 79, + 105, + 100, + 204, + 149, + 56, + 255, + 253, + 141, + 254, + 121, + 104, + 126, + 86, + 180, + 168, + 81, + 209, + 91, + 182, + 60, + 145, + 85, + 225, + 62, + 178, + 34, + 139, + 115, + 188, + 48 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 6.0, + "total": 96.36363, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/receipt.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 4.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 8 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/rhl/runtime.rs": { + "content_hash": [ + 213, + 200, + 189, + 210, + 222, + 185, + 111, + 81, + 220, + 193, + 67, + 174, + 236, + 158, + 220, + 0, + 200, + 18, + 195, + 187, + 215, + 66, + 179, + 111, + 158, + 31, + 18, + 170, + 9, + 189, + 121, + 138 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 95.454544, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/rhl/runtime.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 11 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/rhl/tree.rs": { "content_hash": [ 0, @@ -30597,38 +31253,38 @@ }, "./src/rhl/vocab.rs": { "content_hash": [ - 82, - 157, - 218, - 111, - 150, - 90, - 240, - 213, - 75, - 108, - 129, - 222, - 235, - 215, - 164, - 239, - 158, - 102, + 30, + 231, + 136, + 64, 176, - 103, - 2, - 192, - 203, - 153, - 188, - 235, - 203, - 133, - 180, + 55, + 65, + 151, + 117, + 198, + 230, + 246, + 236, + 130, + 142, + 201, + 212, + 174, + 17, + 106, + 76, + 204, + 176, + 148, + 173, + 32, + 60, 53, - 44, - 136 + 74, + 130, + 101, + 8 ], "score": { "structural_complexity": 25.0, @@ -30668,38 +31324,38 @@ }, "./src/rhl/vocab_cli.rs": { "content_hash": [ - 150, - 136, - 0, - 80, - 6, - 93, - 194, - 220, - 236, - 229, + 154, + 245, + 32, + 137, + 20, + 12, + 46, + 253, + 203, + 22, + 130, + 213, + 75, + 98, 120, - 218, - 255, - 60, - 41, - 166, - 251, - 176, - 79, + 151, 58, - 234, - 54, - 124, - 164, - 255, - 232, - 102, - 67, - 21, - 104, - 139, - 23 + 82, + 125, + 247, + 90, + 251, + 195, + 108, + 209, + 105, + 140, + 31, + 154, + 180, + 203, + 224 ], "score": { "structural_complexity": 25.0, @@ -30721,7 +31377,7 @@ "applied_to": [ "Duplication" ], - "issue": "Found 20 duplicate code patterns" + "issue": "Found 21 duplicate code patterns" } ], "critical_defects_count": 0, @@ -32976,48 +33632,48 @@ }, "./src/runtime/eval_array.rs": { "content_hash": [ - 99, - 216, - 180, - 159, + 236, 241, - 4, - 181, - 167, - 2, - 215, - 163, + 133, + 38, + 236, + 187, + 230, + 248, + 101, + 77, + 124, + 123, + 166, + 159, 223, - 243, - 2, + 196, + 184, 208, - 248, - 13, - 66, + 168, + 55, + 152, + 52, 245, + 131, + 176, + 44, 177, - 207, - 242, - 175, - 173, - 241, - 156, - 110, - 252, - 106, - 65, - 167, - 117 + 180, + 155, + 247, + 216, + 96 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.015873, + "duplication_ratio": 16.124031, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.01587, + "total": 76.12403, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33029,15 +33685,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 71 duplicate code patterns" + "issue": "Found 107 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.984127, + "amount": 3.875969, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 24.9%" + "issue": "Code duplication: 19.4%" }, { "source_metric": "StructuralComplexity", @@ -33045,7 +33701,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 100" + "issue": "High cognitive complexity: 113" }, { "source_metric": "StructuralComplexity", @@ -33053,7 +33709,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 131" + "issue": "High cyclomatic complexity: 167" } ], "critical_defects_count": 0, @@ -33071,48 +33727,48 @@ }, "./src/runtime/eval_builtin.rs": { "content_hash": [ - 144, - 156, - 156, - 68, - 138, - 196, - 168, - 241, - 181, + 66, + 24, + 26, + 160, + 118, + 179, + 80, + 97, + 26, 134, + 194, 115, - 188, - 72, - 104, - 97, - 236, - 74, 201, - 234, + 55, + 17, + 81, + 164, + 32, + 77, 206, - 14, - 252, - 225, - 24, - 91, - 73, - 19, - 189, - 104, - 44, - 78, - 183 + 237, + 210, + 216, + 147, + 54, + 111, + 245, + 127, + 182, + 232, + 231, + 66 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.692015, + "duplication_ratio": 16.67304, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.69202, + "total": 76.67304, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33128,11 +33784,11 @@ }, { "source_metric": "Duplication", - "amount": 3.3079848, + "amount": 3.3269598, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 16.5%" + "issue": "Code duplication: 16.6%" }, { "source_metric": "StructuralComplexity", @@ -33140,7 +33796,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 137" + "issue": "High cognitive complexity: 134" }, { "source_metric": "StructuralComplexity", @@ -33148,7 +33804,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 241" + "issue": "High cyclomatic complexity: 239" } ], "critical_defects_count": 0, @@ -33166,48 +33822,48 @@ }, "./src/runtime/eval_builtin_fs.rs": { "content_hash": [ - 134, - 158, - 181, - 66, + 163, + 130, + 120, + 222, + 168, + 53, + 74, + 214, + 145, + 154, + 187, + 239, + 71, + 245, 252, - 0, - 50, - 254, - 218, - 173, - 29, - 127, - 247, - 169, - 127, - 152, - 204, - 89, + 175, + 132, + 111, 96, - 188, - 166, - 254, - 113, - 199, - 152, - 22, - 194, - 210, - 224, - 48, - 253, - 98 + 156, + 196, + 158, + 94, + 79, + 162, + 42, + 205, + 107, + 216, + 60, + 65, + 128 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.029411, + "duplication_ratio": 15.007385, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.02941, + "total": 75.007385, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33223,11 +33879,11 @@ }, { "source_metric": "Duplication", - "amount": 4.970588, + "amount": 4.9926143, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 24.9%" + "issue": "Code duplication: 25.0%" }, { "source_metric": "StructuralComplexity", @@ -34924,48 +35580,48 @@ }, "./src/runtime/eval_method_dispatch.rs": { "content_hash": [ - 225, - 30, - 118, - 221, - 177, + 127, 124, - 0, + 111, + 187, + 173, + 71, + 41, + 166, + 124, + 31, 64, + 89, + 167, + 220, + 178, + 164, + 134, + 70, + 142, + 149, + 5, + 104, 174, - 37, - 71, - 133, - 249, - 175, - 203, - 212, - 82, - 92, - 210, - 105, - 28, - 16, 246, - 224, - 49, - 108, - 237, - 203, - 199, - 162, - 45, - 9 + 200, + 96, + 86, + 52, + 230, + 16, + 218, + 13 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 19.0, - "duplication_ratio": 15.203762, + "duplication_ratio": 15.150602, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 74.203766, + "total": 74.150604, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -34977,15 +35633,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 68 duplicate code patterns" + "issue": "Found 71 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.7962384, + "amount": 4.8493977, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 24.0%" + "issue": "Code duplication: 24.2%" }, { "source_metric": "StructuralComplexity", @@ -34993,7 +35649,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 81" + "issue": "High cognitive complexity: 83" }, { "source_metric": "StructuralComplexity", @@ -35001,7 +35657,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 122" + "issue": "High cyclomatic complexity: 125" }, { "source_metric": "SemanticComplexity", @@ -35028,47 +35684,47 @@ "./src/runtime/eval_operations.rs": { "content_hash": [ 145, - 0, - 130, - 59, + 124, + 227, + 136, + 252, + 51, + 231, + 54, + 93, + 78, + 231, + 99, + 44, + 218, + 82, + 27, + 147, + 48, + 54, + 189, + 218, + 183, + 57, + 254, 64, + 192, 17, - 240, - 78, - 127, - 180, - 3, - 149, - 135, - 213, - 130, - 194, - 61, + 193, + 184, + 34, 174, - 167, - 104, - 241, - 145, - 146, - 68, - 3, - 37, - 150, - 167, - 227, - 96, - 172, - 117 + 16 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.770863, + "duplication_ratio": 15.779817, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.77086, + "total": 75.779816, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -35084,7 +35740,7 @@ }, { "source_metric": "Duplication", - "amount": 4.2291374, + "amount": 4.2201834, "applied_to": [ "Duplication" ], @@ -35104,7 +35760,102 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 148" + "issue": "High cyclomatic complexity: 149" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/runtime/eval_option_methods.rs": { + "content_hash": [ + 93, + 22, + 85, + 26, + 28, + 114, + 202, + 203, + 247, + 239, + 42, + 220, + 195, + 231, + 148, + 254, + 245, + 49, + 142, + 23, + 226, + 169, + 38, + 162, + 71, + 37, + 47, + 205, + 200, + 77, + 25, + 186 + ], + "score": { + "structural_complexity": 8.4, + "semantic_complexity": 20.0, + "duplication_ratio": 17.311476, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 85.71147, + "grade": "A-", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/runtime/eval_option_methods.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 25 duplicate code patterns" + }, + { + "source_metric": "Duplication", + "amount": 2.6885247, + "applied_to": [ + "Duplication" + ], + "issue": "Code duplication: 13.4%" + }, + { + "source_metric": "StructuralComplexity", + "amount": 2.1000001, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 22" + }, + { + "source_metric": "StructuralComplexity", + "amount": 14.5, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 59" } ], "critical_defects_count": 0, @@ -35502,38 +36253,38 @@ }, "./src/runtime/eval_string_methods.rs": { "content_hash": [ - 127, - 5, - 254, - 134, - 19, - 214, - 176, - 86, - 48, - 184, - 115, - 187, - 179, - 171, - 40, - 222, - 19, - 112, - 110, - 120, - 240, - 124, - 249, - 121, - 231, - 91, - 54, - 158, - 198, + 9, + 43, + 96, + 42, + 216, + 156, + 22, + 209, + 157, 94, - 140, - 70 + 10, + 203, + 42, + 82, + 162, + 114, + 157, + 165, + 106, + 204, + 226, + 195, + 117, + 38, + 216, + 66, + 122, + 206, + 82, + 76, + 142, + 180 ], "score": { "structural_complexity": 0.0, @@ -36151,38 +36902,38 @@ }, "./src/runtime/interpreter_control_flow.rs": { "content_hash": [ - 254, - 239, - 119, - 191, - 31, + 215, + 40, + 223, + 133, + 42, + 246, + 3, + 200, + 109, + 228, + 162, + 246, + 208, + 209, + 45, + 213, + 92, + 73, + 193, + 3, + 103, + 233, + 152, + 142, + 234, + 202, + 108, + 230, + 65, 73, - 217, - 197, - 249, - 154, - 192, 5, - 67, - 135, - 238, - 16, - 214, - 102, - 161, - 127, - 23, - 96, - 184, - 204, - 139, - 42, - 117, - 164, - 69, - 28, - 82, - 202 + 132 ], "score": { "structural_complexity": 0.0, @@ -36349,48 +37100,48 @@ }, "./src/runtime/interpreter_functions.rs": { "content_hash": [ - 119, - 76, - 12, - 224, - 8, - 143, - 17, - 197, - 126, - 20, - 255, - 29, - 209, + 82, + 198, + 254, + 164, + 239, + 157, + 93, + 221, + 146, + 7, + 102, + 248, + 7, + 166, + 232, + 103, + 155, + 246, + 141, + 100, 208, - 25, - 229, - 187, - 173, - 37, - 156, - 194, - 176, - 241, - 46, - 194, - 253, - 95, - 119, - 49, - 95, - 135, - 72 + 129, + 216, + 179, + 203, + 132, + 215, + 143, + 81, + 60, + 203, + 30 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.461182, + "duplication_ratio": 14.469526, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 74.46118, + "total": 74.46953, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -36410,11 +37161,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 98 duplicate code patterns" + "issue": "Found 103 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.5388184, + "amount": 5.530474, "applied_to": [ "Duplication" ], @@ -36426,15 +37177,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 114" + "issue": "High cognitive complexity: 116" }, { "source_metric": "StructuralComplexity", - "amount": 13.0, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 56" + "issue": "High cyclomatic complexity: 64" } ], "critical_defects_count": 0, @@ -36625,48 +37376,48 @@ }, "./src/runtime/interpreter_methods_actor.rs": { "content_hash": [ - 104, - 239, - 153, - 135, - 143, - 185, - 32, - 184, - 218, + 103, + 79, + 111, 13, - 140, - 71, - 199, - 17, - 237, - 162, - 14, - 239, + 208, + 118, + 230, + 89, + 25, + 249, + 234, + 178, + 185, + 58, + 143, 52, - 17, - 144, - 80, - 80, - 152, - 122, - 15, - 42, + 221, + 151, + 176, + 134, + 116, + 43, + 138, + 73, + 208, 188, - 252, + 224, + 7, + 117, + 147, 103, - 241, - 8 + 143 ], "score": { "structural_complexity": 9.5, "semantic_complexity": 20.0, - "duplication_ratio": 17.407408, + "duplication_ratio": 17.421053, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 86.90741, + "total": 86.92105, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -36682,11 +37433,11 @@ }, { "source_metric": "Duplication", - "amount": 2.5925925, + "amount": 2.5789473, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.0%" + "issue": "Code duplication: 12.9%" }, { "source_metric": "StructuralComplexity", @@ -36720,76 +37471,68 @@ }, "./src/runtime/interpreter_methods_dispatch.rs": { "content_hash": [ - 33, - 121, - 164, - 105, - 218, - 76, - 252, - 192, - 108, - 57, + 173, + 241, + 155, + 127, + 201, + 26, + 201, + 17, + 213, + 107, + 140, + 113, + 132, + 134, + 133, + 253, + 113, + 193, + 237, + 243, + 185, + 113, + 64, + 209, 234, - 53, - 119, - 244, - 4, - 56, - 6, - 219, - 224, - 102, - 217, - 105, - 255, - 194, - 61, - 16, - 205, - 232, - 28, - 19, - 79, + 131, + 50, + 13, + 63, + 71, + 15, 102 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 13.974066, - "coupling_score": 11.4, + "duplication_ratio": 14.254144, + "coupling_score": 11.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 70.37407, + "total": 70.25414, "grade": "B-", "confidence": 1.0, "language": "Rust", "file_path": "./src/runtime/interpreter_methods_dispatch.rs", "penalties_applied": [ - { - "source_metric": "StructuralComplexity", - "amount": 2.0, - "applied_to": [ - "StructuralComplexity" - ], - "issue": "Deep nesting: 6 levels" - }, { "source_metric": "Duplication", "amount": 5.0, "applied_to": [ "Duplication" ], - "issue": "Found 175 duplicate code patterns" + "issue": "Found 165 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.025934, + "amount": 5.7458563, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 30.1%" + "issue": "Code duplication: 28.7%" }, { "source_metric": "StructuralComplexity", @@ -36797,7 +37540,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 122" + "issue": "High cognitive complexity: 59" }, { "source_metric": "StructuralComplexity", @@ -36805,15 +37548,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 68" + "issue": "High cyclomatic complexity: 73" }, { "source_metric": "Coupling", - "amount": 3.6000001, + "amount": 4.0, "applied_to": [ "Coupling" ], - "issue": "Too many imports: 38" + "issue": "Too many imports: 40" } ], "critical_defects_count": 0, @@ -37100,48 +37843,48 @@ }, "./src/runtime/interpreter_misc_eval.rs": { "content_hash": [ - 241, - 131, - 128, - 101, - 104, - 2, - 83, - 210, - 214, - 64, - 139, - 23, - 99, - 135, - 150, - 69, - 63, - 241, - 41, - 70, - 64, - 95, - 119, + 161, + 62, + 56, + 225, + 38, + 172, + 125, + 140, + 251, + 38, + 242, + 238, + 43, + 114, + 6, + 54, + 133, + 141, + 25, + 248, + 97, + 173, + 44, + 56, + 233, + 132, + 171, 45, - 176, - 106, - 217, - 211, - 9, - 176, - 167, - 173 + 108, + 202, + 113, + 142 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 18.5, - "duplication_ratio": 16.703568, + "duplication_ratio": 16.57444, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.20357, + "total": 75.07444, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -37153,15 +37896,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 105 duplicate code patterns" + "issue": "Found 102 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.296433, + "amount": 3.42556, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 16.5%" + "issue": "Code duplication: 17.1%" }, { "source_metric": "StructuralComplexity", @@ -37169,7 +37912,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 113" + "issue": "High cognitive complexity: 99" }, { "source_metric": "StructuralComplexity", @@ -37177,7 +37920,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 126" + "issue": "High cyclomatic complexity: 117" }, { "source_metric": "SemanticComplexity", @@ -37456,48 +38199,48 @@ }, "./src/runtime/interpreter_types_class.rs": { "content_hash": [ - 224, - 22, + 150, + 140, + 146, + 6, + 5, + 240, 215, + 14, + 96, + 141, + 141, + 179, 161, - 40, - 155, - 41, - 177, - 103, - 249, - 171, - 145, - 0, - 139, - 235, - 254, - 43, - 17, - 68, - 56, - 222, - 229, - 15, - 137, - 246, - 216, - 178, - 46, - 149, - 112, - 236, - 127 + 214, + 197, + 108, + 247, + 244, + 52, + 65, + 247, + 105, + 241, + 37, + 232, + 111, + 7, + 189, + 98, + 132, + 20, + 107 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.80498, + "duplication_ratio": 16.882591, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.80498, + "total": 76.88259, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -37517,15 +38260,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 62 duplicate code patterns" + "issue": "Found 61 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.195021, + "amount": 3.117409, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 16.0%" + "issue": "Code duplication: 15.6%" }, { "source_metric": "StructuralComplexity", @@ -37533,7 +38276,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 181" + "issue": "High cognitive complexity: 177" }, { "source_metric": "StructuralComplexity", @@ -38096,48 +38839,48 @@ }, "./src/runtime/mod.rs": { "content_hash": [ - 88, - 26, - 157, - 61, - 98, - 235, - 223, + 138, + 209, + 160, + 254, + 233, + 168, + 64, 16, - 27, - 60, - 244, - 250, - 27, - 124, - 140, - 175, - 214, - 153, - 103, - 14, - 48, - 227, - 110, - 95, - 207, - 178, - 113, - 150, - 65, - 161, - 38, - 110 + 10, + 55, + 69, + 255, + 4, + 121, + 63, + 123, + 192, + 53, + 176, + 13, + 160, + 238, + 142, + 45, + 243, + 213, + 15, + 99, + 94, + 62, + 156, + 90 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.366972, + "duplication_ratio": 15.377575, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 91.2427, + "total": 91.25234, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -38153,11 +38896,11 @@ }, { "source_metric": "Duplication", - "amount": 4.6330276, + "amount": 4.6224256, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 23.2%" + "issue": "Code duplication: 23.1%" } ], "critical_defects_count": 0, @@ -38816,48 +39559,48 @@ }, "./src/runtime/repl/evaluation.rs": { "content_hash": [ + 44, + 74, + 136, + 253, 83, - 32, - 118, - 98, - 37, - 80, - 57, - 87, - 140, - 224, - 160, - 238, - 102, - 94, - 205, - 165, - 146, - 197, - 68, - 225, - 241, - 205, + 233, + 166, + 23, 138, - 89, 77, - 34, - 205, - 102, - 30, - 33, - 157, - 136 + 131, + 140, + 240, + 11, + 241, + 222, + 173, + 3, + 179, + 82, + 202, + 80, + 142, + 179, + 245, + 227, + 56, + 47, + 210, + 70, + 199, + 85 ], "score": { - "structural_complexity": 1.2999992, + "structural_complexity": 0.39999962, "semantic_complexity": 20.0, - "duplication_ratio": 16.987951, + "duplication_ratio": 17.1537, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 78.28795, + "total": 77.553696, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -38873,19 +39616,19 @@ }, { "source_metric": "Duplication", - "amount": 3.0120482, + "amount": 2.8462996, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 15.1%" + "issue": "Code duplication: 14.2%" }, { "source_metric": "StructuralComplexity", - "amount": 8.700001, + "amount": 9.6, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 44" + "issue": "High cognitive complexity: 47" }, { "source_metric": "StructuralComplexity", @@ -38893,7 +39636,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 75" + "issue": "High cyclomatic complexity: 79" } ], "critical_defects_count": 0, @@ -39960,49 +40703,49 @@ }, "./src/runtime/value.rs": { "content_hash": [ - 230, - 96, - 88, - 3, + 43, + 11, 91, - 102, - 214, - 29, - 178, - 151, - 92, - 212, - 8, - 159, - 96, - 109, - 136, - 125, - 127, - 57, - 219, - 108, - 159, - 22, - 177, - 71, + 215, + 99, + 13, + 95, + 51, + 143, + 117, + 200, + 53, + 105, + 10, + 141, + 46, + 152, + 228, 91, - 248, - 42, - 205, - 250, - 210 + 40, + 126, + 185, + 220, + 244, + 188, + 126, + 162, + 208, + 93, + 237, + 226, + 176 ], "score": { - "structural_complexity": 21.5, + "structural_complexity": 19.5, "semantic_complexity": 20.0, "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.27273, - "grade": "A", + "total": 99.5, + "grade": "A+", "confidence": 1.0, "language": "Rust", "file_path": "./src/runtime/value.rs", @@ -40013,15 +40756,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 12 duplicate code patterns" + "issue": "Found 15 duplicate code patterns" }, { "source_metric": "StructuralComplexity", - "amount": 3.5, + "amount": 5.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 37" + "issue": "High cyclomatic complexity: 41" } ], "critical_defects_count": 0, @@ -40039,49 +40782,49 @@ }, "./src/runtime/value_format.rs": { "content_hash": [ - 244, - 26, - 54, - 42, + 121, + 135, + 132, + 254, + 90, + 24, + 94, + 254, 83, - 128, - 153, - 106, - 197, + 102, + 160, 57, - 53, - 152, - 194, - 175, - 174, - 145, - 101, - 74, - 39, - 45, - 167, - 215, - 252, - 226, - 185, - 23, - 78, - 96, - 64, - 203, - 189, - 200 + 28, + 177, + 173, + 13, + 85, + 255, + 66, + 110, + 180, + 91, + 246, + 19, + 113, + 199, + 19, + 199, + 205, + 191, + 186, + 92 ], "score": { - "structural_complexity": 14.599998, + "structural_complexity": 8.0, "semantic_complexity": 20.0, "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 94.6, - "grade": "A", + "total": 88.0, + "grade": "A-", "confidence": 1.0, "language": "Rust", "file_path": "./src/runtime/value_format.rs", @@ -40100,15 +40843,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 30 duplicate code patterns" + "issue": "Found 33 duplicate code patterns" }, { "source_metric": "StructuralComplexity", - "amount": 8.400001, + "amount": 10.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 43" + "issue": "High cognitive complexity: 50" + }, + { + "source_metric": "StructuralComplexity", + "amount": 5.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 40" } ], "critical_defects_count": 0, @@ -40300,48 +41051,48 @@ }, "./src/server/watcher.rs": { "content_hash": [ - 128, - 138, - 162, - 107, - 156, - 92, - 30, - 145, - 190, - 93, - 114, - 66, + 135, + 46, + 184, + 173, + 85, + 74, + 246, + 78, + 55, + 160, + 176, + 191, 16, - 112, - 1, - 151, - 174, - 192, - 162, - 105, - 169, - 121, - 231, - 105, - 44, - 157, - 151, - 43, - 187, - 198, - 24, - 114 + 229, + 159, + 218, + 51, + 104, + 219, + 178, + 63, + 160, + 156, + 219, + 116, + 200, + 152, + 186, + 205, + 177, + 60, + 187 ], "score": { - "structural_complexity": 25.0, + "structural_complexity": 24.7, "semantic_complexity": 20.0, - "duplication_ratio": 16.556292, + "duplication_ratio": 17.41573, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.3239, + "total": 92.83248, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -40357,11 +41108,19 @@ }, { "source_metric": "Duplication", - "amount": 3.4437087, + "amount": 2.5842695, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 17.2%" + "issue": "Code duplication: 12.9%" + }, + { + "source_metric": "StructuralComplexity", + "amount": 0.3, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 16" } ], "critical_defects_count": 0, @@ -40906,6 +41665,77 @@ }, "git_context": null }, + "./src/stdlib/hash.rs": { + "content_hash": [ + 245, + 136, + 56, + 33, + 87, + 85, + 153, + 67, + 145, + 119, + 239, + 236, + 114, + 249, + 67, + 111, + 131, + 234, + 2, + 234, + 140, + 58, + 157, + 139, + 238, + 1, + 24, + 139, + 216, + 107, + 167, + 149 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 7.0, + "total": 97.27273, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/stdlib/hash.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 3.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 6 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/stdlib/html.rs": { "content_hash": [ 79, @@ -41232,38 +42062,38 @@ }, "./src/stdlib/mod.rs": { "content_hash": [ - 55, - 9, - 236, - 68, - 184, - 210, - 158, - 74, - 228, - 40, - 201, - 244, - 244, + 117, + 213, + 217, + 29, + 113, + 111, + 139, + 37, + 26, + 212, + 6, + 155, 73, - 115, - 239, - 105, - 78, - 201, - 222, - 220, - 143, - 120, - 4, - 39, - 144, - 60, - 209, - 122, - 43, - 181, - 140 + 91, + 185, + 32, + 197, + 242, + 134, + 168, + 99, + 86, + 184, + 241, + 140, + 194, + 103, + 47, + 69, + 35, + 172, + 190 ], "score": { "structural_complexity": 25.0, @@ -41777,38 +42607,38 @@ }, "./src/stdlib/trueno_bridge.rs": { "content_hash": [ - 208, - 228, - 201, - 153, - 221, - 3, - 80, - 80, - 80, - 196, - 29, - 242, - 56, + 180, + 174, + 192, + 124, + 172, + 94, + 141, + 120, + 8, + 132, + 61, + 69, + 97, + 94, + 180, + 86, + 216, + 42, + 200, + 207, + 247, + 224, 5, + 123, 223, - 13, - 158, - 239, - 62, - 175, - 108, - 238, - 75, - 34, - 70, - 240, - 146, - 152, - 113, - 70, - 75, - 31 + 15, + 78, + 171, + 176, + 9, + 221, + 44 ], "score": { "structural_complexity": 25.0, @@ -44144,14 +44974,14 @@ } }, "summary": { - "total_files": 519, - "avg_score": 90.31551, + "total_files": 529, + "avg_score": 90.30044, "grade_distribution": { - "A+": 189, + "A+": 194, "A": 176, - "A-": 30, - "B+": 28, - "B": 67, + "A-": 33, + "B+": 29, + "B": 68, "B-": 26, "C": 1, "F": 2 @@ -44159,7 +44989,7 @@ "languages": { "JavaScript": 14, "Python": 4, - "Rust": 497, + "Rust": 507, "TypeScript": 4 } } diff --git a/tests/extendkw_1_keyword_methods.rs b/tests/extendkw_1_keyword_methods.rs new file mode 100644 index 000000000..60f56b5dd --- /dev/null +++ b/tests/extendkw_1_keyword_methods.rs @@ -0,0 +1,243 @@ +//! EXTENDKW-1: a keyword token after `.` is a method or field name. +//! +//! `v.extend([3])` failed with "Expected method name, tuple index, or 'await' +//! after '.'": `extend` lexes as `Token::Extend` (Ruchy's extension-block +//! keyword), and the postfix `.` parser only accepted `Token::Identifier` +//! (plus `send`/`ask`). The same held for `o.type`, `o.match(x)`, +//! `o.default()`, `o.from(x)`, and every other keyword. +//! +//! After `.` (and `?.`) a keyword is now the member name spelled as its +//! source text. `x.await` stays an await expression. Transpiled Rust escapes a +//! Rust reserved word as a raw identifier (`o.r#match(1)`). + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(30)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn check(src: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("check").arg(file); + stdout_of(cmd, &format!("ruchy check on:\n{src}\n")); +} + +fn transpile(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, &format!("ruchy transpile on:\n{src}\n")) +} + +fn transpile_str(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + transpile(dir.path(), src) +} + +fn rustc_run(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("main.rs"); + let bin = dir.join("prog_bin"); + std::fs::write(&main_rs, rust).expect("write main.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "transpiled binary") +} + +/// Keywords that are Rust reserved words: transpiled as `r#kw`. +const RUST_RESERVED: &[&str] = &[ + "match", "type", "loop", "impl", "mod", "use", "as", "in", "where", "static", "const", "trait", + "struct", "enum", "try", "yield", "final", "abstract", "override", "async", "return", "break", + "continue", "if", "else", "for", "while", "let", "fn", "pub", "mut", "unsafe", +]; + +/// Ruchy-only keywords: plain Rust identifiers. +const RUCHY_ONLY: &[&str] = &[ + "extend", + "from", + "default", + "module", + "var", + "with", + "import", + "export", + "class", + "handle", + "handler", + "effect", + "property", + "private", + "protected", + "sealed", + "mixin", + "operator", + "interface", + "implements", + "receive", + "spawn", + "actor", + "lazy", + "catch", + "finally", + "throw", + "signal", + "infra", + "requires", + "ensures", + "invariant", + "decreases", + "fun", + "send", + "ask", +]; + +fn rust_member(kw: &str) -> String { + if RUST_RESERVED.contains(&kw) { + format!("r#{kw}") + } else { + kw.to_string() + } +} + +fn all_keywords() -> impl Iterator { + RUST_RESERVED.iter().chain(RUCHY_ONLY.iter()).copied() +} + +/// Strip whitespace so `o . r#match (1)` and `o.r#match(1)` compare equal. +fn squash(s: &str) -> String { + s.chars().filter(|c| !c.is_whitespace()).collect() +} + +fn method_src(kw: &str) -> String { + format!("fun probe(o: Widget) -> i32 {{\n o.{kw}(1)\n}}\n") +} + +fn field_src(kw: &str) -> String { + format!("fun probe(o: Widget) -> i32 {{\n o.{kw}\n}}\n") +} + +#[test] +fn test_extendkw_1_01_vec_extend_checks_clean() { + check("let mut v = [1, 2]\nv.extend([3])\nprintln(v.len())\n"); +} + +#[test] +fn test_extendkw_1_02_vec_extend_transpiles_to_method_call() { + let rust = transpile_str( + "fun main() {\n let mut v = [1, 2]\n v.extend([3])\n println(v.len())\n}\n", + ); + assert!( + squash(&rust).contains("v.extend("), + "expected a v.extend(..) call in:\n{rust}" + ); +} + +#[test] +fn test_extendkw_1_03_vec_extend_compiles_with_rustc() { + // A struct field typed Vec transpiles its literal to vec![..] (VECLIT-1); + // a bare `let mut v = [1, 2]` still transpiles to a fixed-size Rust array. + let src = "struct Bag {\n items: Vec,\n}\n\nfun main() {\n let mut b = Bag { items: [1, 2] }\n b.items.extend([3])\n println(b.items.len())\n}\n"; + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), src); + assert!( + squash(&rust).contains("b.items.extend([3])"), + "expected b.items.extend([3]) in:\n{rust}" + ); + let out = rustc_run(dir.path(), &rust); + assert_eq!(out.trim(), "3", "rust:\n{rust}"); +} + +#[test] +fn test_extendkw_1_04_every_keyword_is_a_method_name() { + for kw in all_keywords() { + let src = method_src(kw); + check(&src); + let rust = squash(&transpile_str(&src)); + let want = format!("o.{}(1", rust_member(kw)); + assert!( + rust.contains(&want), + "`o.{kw}(1)`: expected `{want}` in:\n{rust}" + ); + } +} + +#[test] +fn test_extendkw_1_05_every_keyword_is_a_field_name() { + for kw in all_keywords() { + check(&field_src(kw)); + } + // Only Ruchy-only keywords are asserted through transpile: a field access + // named by a Rust reserved word is emitted without r# by the transpiler. + for kw in RUCHY_ONLY.iter().copied() { + let rust = squash(&transpile_str(&field_src(kw))); + let want = format!("o.{}", rust_member(kw)); + assert!( + rust.contains(&want), + "`o.{kw}`: expected `{want}` in:\n{rust}" + ); + } +} + +#[test] +fn test_extendkw_1_06_keyword_method_chains() { + let src = "fun probe(o: Widget) -> i32 {\n o.extend(1).match(2).default().type(3)\n}\n"; + check(src); + let rust = squash(&transpile_str(src)); + assert!( + rust.contains("o.extend(1).r#match(2).default().r#type(3)"), + "chain lost a member:\n{rust}" + ); +} + +#[test] +fn test_extendkw_1_07_await_stays_await() { + let src = "async fun probe(x: i32) -> i32 {\n x.await\n}\n"; + check(src); + let rust = squash(&transpile_str(src)); + assert!(rust.contains("x.await"), "expected `x.await` in:\n{rust}"); + assert!(!rust.contains("r#await"), "await became a member:\n{rust}"); +} + +#[test] +fn test_extendkw_1_08_keyword_after_optional_chain() { + check("let o = {\"extend\": 1}\nlet a = o?.extend\nlet b = o?.type\nprintln(a)\n"); +} + +#[test] +fn test_extendkw_1_09_tuple_index_and_identifier_members_unchanged() { + let src = "fun probe(t: (i32, i32), o: Widget) -> i32 {\n t.0 + o.len() + o.size\n}\n"; + check(src); + let rust = squash(&transpile_str(src)); + for want in ["t.0", "o.len()", "o.size"] { + assert!(rust.contains(want), "expected `{want}` in:\n{rust}"); + } +} From 719dabe543ec0ed939678a316434c9351da5089c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:52:37 +0200 Subject: [PATCH 049/106] EXTENDKW-1 (GREEN): a keyword after '.' or '?.' is a member name - src/frontend/parser/member_names.rs: table of keyword tokens usable as a method/field name, spelled as source text; member_name() (complexity 2) also covers Identifier; await/self/super/crate excluded - parse_method_call / parse_optional_method_call consult peek_member_name() before the tuple-index arm; the send/ask arms are folded into the table - x.await is still matched first and stays an await expression - Method calls on reserved words transpile as r#kw (existing safe_ident); reserved field names are not escaped by the transpiler (reported) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2 +- src/frontend/parser/functions.rs | 47 ++++------ src/frontend/parser/member_names.rs | 141 ++++++++++++++++++++++++++++ src/frontend/parser/mod.rs | 1 + 4 files changed, 160 insertions(+), 31 deletions(-) create mode 100644 src/frontend/parser/member_names.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index c3883e0fc..5e6fbc7d6 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T05:58:21.272608795Z", + "created_at": "2026-09-24T05:58:22.268670176Z", "git_context": null, "files": { "./build.rs": { diff --git a/src/frontend/parser/functions.rs b/src/frontend/parser/functions.rs index 572980a3c..1b7d56dfc 100644 --- a/src/frontend/parser/functions.rs +++ b/src/frontend/parser/functions.rs @@ -449,22 +449,12 @@ pub fn parse_method_call(state: &mut ParserState, receiver: Expr) -> Result { - let method = name.clone(); - state.tokens.advance(); - parse_method_or_field_access(state, receiver, method, name_end) - } - Some((Token::Send, _)) => { - // Handle 'send' as a method name (for actors) - state.tokens.advance(); - parse_method_or_field_access(state, receiver, "send".to_string(), name_end) - } - Some((Token::Ask, _)) => { - // Handle 'ask' as a method name (for actors) - state.tokens.advance(); - parse_method_or_field_access(state, receiver, "ask".to_string(), name_end) - } Some((Token::Integer(index), _)) => { // Handle tuple access like t.0, t.1, etc. let index = index.clone(); @@ -476,26 +466,23 @@ pub fn parse_method_call(state: &mut ParserState, receiver: Expr) -> Result Option { + state + .tokens + .peek() + .and_then(|(token, _)| super::member_names::member_name(token)) +} pub fn parse_optional_method_call(state: &mut ParserState, receiver: Expr) -> Result { // Skip any comments between '?.' and method name (PARSER-053) state.skip_comments(); // Parse method name or tuple index for optional chaining + // EXTENDKW-1: an identifier or any keyword (`extend`, `type`, `match`, ...) + if let Some(method) = peek_member_name(state) { + state.tokens.advance(); + return parse_optional_method_or_field_access(state, receiver, method); + } match state.tokens.peek() { - Some((Token::Identifier(name), _)) => { - let method = name.clone(); - state.tokens.advance(); - parse_optional_method_or_field_access(state, receiver, method) - } - Some((Token::Send, _)) => { - // Handle 'send' as a method name (for actors) - state.tokens.advance(); - parse_optional_method_or_field_access(state, receiver, "send".to_string()) - } - Some((Token::Ask, _)) => { - // Handle 'ask' as a method name (for actors) - state.tokens.advance(); - parse_optional_method_or_field_access(state, receiver, "ask".to_string()) - } Some((Token::Integer(index), _)) => { // Handle optional tuple access like t?.0, t?.1, etc. let index = index.clone(); diff --git a/src/frontend/parser/member_names.rs b/src/frontend/parser/member_names.rs new file mode 100644 index 000000000..96869ea62 --- /dev/null +++ b/src/frontend/parser/member_names.rs @@ -0,0 +1,141 @@ +//! EXTENDKW-1: member names after `.` and `?.`. +//! +//! A keyword token in member position (`v.extend(x)`, `o.type`, `o.match(p)`) +//! names a method or field; it is spelled as its source text. `await` is not +//! listed: `x.await` is an await expression and is handled before this lookup. +//! `self`/`super`/`crate`/`Self` are not listed: Rust cannot name a member +//! with them, not even as a raw identifier. +use super::Token; + +/// Keyword tokens accepted as a member name, paired with their source text. +static KEYWORD_MEMBERS: &[(Token, &str)] = &[ + (Token::Extend, "extend"), + (Token::Match, "match"), + (Token::Type, "type"), + (Token::Loop, "loop"), + (Token::Impl, "impl"), + (Token::Mod, "mod"), + (Token::Use, "use"), + (Token::As, "as"), + (Token::In, "in"), + (Token::Where, "where"), + (Token::Static, "static"), + (Token::Const, "const"), + (Token::Trait, "trait"), + (Token::Struct, "struct"), + (Token::Enum, "enum"), + (Token::Try, "try"), + (Token::Yield, "yield"), + (Token::Final, "final"), + (Token::Abstract, "abstract"), + (Token::Override, "override"), + (Token::Async, "async"), + (Token::Return, "return"), + (Token::Break, "break"), + (Token::Continue, "continue"), + (Token::If, "if"), + (Token::Else, "else"), + (Token::For, "for"), + (Token::While, "while"), + (Token::Let, "let"), + (Token::Fn, "fn"), + (Token::Pub, "pub"), + (Token::Mut, "mut"), + (Token::Unsafe, "unsafe"), + (Token::From, "from"), + (Token::Default, "default"), + (Token::Module, "module"), + (Token::Var, "var"), + (Token::With, "with"), + (Token::Import, "import"), + (Token::Export, "export"), + (Token::Class, "class"), + (Token::Handle, "handle"), + (Token::Handler, "handler"), + (Token::Effect, "effect"), + (Token::Property, "property"), + (Token::Private, "private"), + (Token::Protected, "protected"), + (Token::Sealed, "sealed"), + (Token::Mixin, "mixin"), + (Token::Operator, "operator"), + (Token::Interface, "interface"), + (Token::Implements, "implements"), + (Token::Receive, "receive"), + (Token::Spawn, "spawn"), + (Token::Actor, "actor"), + (Token::Lazy, "lazy"), + (Token::Catch, "catch"), + (Token::Finally, "finally"), + (Token::Throw, "throw"), + (Token::Signal, "signal"), + (Token::Infra, "infra"), + (Token::Requires, "requires"), + (Token::Ensures, "ensures"), + (Token::Invariant_, "invariant"), + (Token::Decreases, "decreases"), + (Token::Fun, "fun"), + (Token::Send, "send"), + (Token::Ask, "ask"), +]; + +/// Source text of a keyword token usable as a member name (complexity: 1). +fn keyword_member_name(token: &Token) -> Option<&'static str> { + KEYWORD_MEMBERS + .iter() + .find(|(keyword, _)| keyword == token) + .map(|(_, name)| *name) +} + +/// Member name spelled by `token`: an identifier or a keyword (complexity: 2). +pub(super) fn member_name(token: &Token) -> Option { + match token { + Token::Identifier(name) => Some(name.clone()), + other => keyword_member_name(other).map(str::to_string), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extendkw_1_member_name_identifier() { + assert_eq!( + member_name(&Token::Identifier("len".to_string())), + Some("len".to_string()) + ); + } + + #[test] + fn test_extendkw_1_member_name_keywords() { + assert_eq!(member_name(&Token::Extend), Some("extend".to_string())); + assert_eq!(member_name(&Token::Match), Some("match".to_string())); + assert_eq!( + member_name(&Token::Invariant_), + Some("invariant".to_string()) + ); + } + + #[test] + fn test_extendkw_1_member_name_rejects_await_and_paths() { + for token in [ + Token::Await, + Token::Self_, + Token::Super, + Token::Crate, + Token::Integer("0".to_string()), + ] { + assert_eq!(member_name(&token), None, "{token:?}"); + } + } + + #[test] + fn test_extendkw_1_keyword_member_spelling_matches_lexer() { + for (token, name) in KEYWORD_MEMBERS { + let mut stream = crate::frontend::lexer::TokenStream::new(name); + let first = stream.next().map(|(t, _)| t); + assert_eq!(first.as_ref(), Some(token), "{name}"); + } + } +} diff --git a/src/frontend/parser/mod.rs b/src/frontend/parser/mod.rs index 4bf10cc4b..097e01388 100644 --- a/src/frontend/parser/mod.rs +++ b/src/frontend/parser/mod.rs @@ -51,6 +51,7 @@ mod expressions; mod functions; mod imports; mod macro_parsing; +mod member_names; mod operator_precedence; mod types; mod utils; From 95bdb8dd18e48ec61299b833d8a439d2154c06ea Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 07:59:12 +0200 Subject: [PATCH 050/106] RHLGA-1: file LETVEC-1 and RESFIELD-1 Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index d5091b15a..103e3c8be 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4150,3 +4150,45 @@ roadmap: - kind:code - runtime - filed-by:RHLGA-1 +- id: LETVEC-1 + github_issue: null + item_type: task + title: 'LETVEC-1: let mut v = [1, 2]; v.push(3) transpiles to a fixed-size Rust array and fails rustc E0599' + status: inprogress + priority: critical + assigned_to: null + created: 2026-09-24T08:30:00Z + updated: 2026-09-24T08:30:00Z + spec: null + acceptance_criteria: + - 'Repro: fun main() { let mut v = [1, 2]; v.push(3); println(v.len()) } -> transpile emits let mut v = [1, 2]; ruchy compile fails E0599 no method push for [{integer}; 2]. Same with a Vec annotation. Pre-existing in 4.2.1' + - 'Fix: an array literal bound by let is emitted as vec![...] when the binding is annotated Vec<...> or the binding is later used with a Vec-only method (push, pop, extend, insert, remove, clear, truncate, append, dedup, retain, drain, resize) or passed where the interpreter semantics require growth; otherwise unchanged' + - 'Acceptance: tests/letvec_1_growable_locals.rs, transpile + rustc-compile + run for each Vec-only method on an unannotated and an annotated local, top-level script and fun main, and a fixed array that is never grown stays an array' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 +- id: RESFIELD-1 + github_issue: null + item_type: task + title: 'RESFIELD-1: a field named by a Rust reserved word (o.match, o.type) transpiles to invalid Rust' + status: inprogress + priority: critical + assigned_to: null + created: 2026-09-24T08:30:00Z + updated: 2026-09-24T08:30:00Z + spec: null + acceptance_criteria: + - 'Repro (after EXTENDKW-1): o.type passes ruchy check, ruchy transpile fails "Failed to parse generated tokens as Rust syntax"; transpile_field_access in expressions_helpers/field_access.rs builds the name with format_ident! instead of safe_ident' + - 'Fix: field access, struct field declarations and struct literal field names use safe_ident (r#kw)' + - 'Acceptance: tests/extendkw_1_keyword_methods.rs field test asserts transpile + rustc for every reserved-word field name' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 From c679a95d843277989f47b329905da8f36a6215e3 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 08:02:49 +0200 Subject: [PATCH 051/106] LETVEC-1/RESFIELD-1 (RED): growable let-bound arrays; reserved-word field access - tests/letvec_1_growable_locals.rs: each Vec-only method on an unannotated and a Vec-annotated local, fun main and top-level script: transpile emits vec![..], ruchy compile (rustc) + run prints the interpreter's output; never-grown and sort/reverse-only arrays stay fixed arrays - tests/extendkw_1_keyword_methods.rs: every keyword field name transpiles (r#kw for Rust reserved words); o.type + o.match compiles through rustc Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/extendkw_1_keyword_methods.rs | 24 ++- tests/letvec_1_growable_locals.rs | 245 ++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 tests/letvec_1_growable_locals.rs diff --git a/tests/extendkw_1_keyword_methods.rs b/tests/extendkw_1_keyword_methods.rs index 60f56b5dd..522ca36f6 100644 --- a/tests/extendkw_1_keyword_methods.rs +++ b/tests/extendkw_1_keyword_methods.rs @@ -192,13 +192,11 @@ fn test_extendkw_1_04_every_keyword_is_a_method_name() { #[test] fn test_extendkw_1_05_every_keyword_is_a_field_name() { + // RESFIELD-1: a field named by a Rust reserved word transpiles to `r#kw`. for kw in all_keywords() { - check(&field_src(kw)); - } - // Only Ruchy-only keywords are asserted through transpile: a field access - // named by a Rust reserved word is emitted without r# by the transpiler. - for kw in RUCHY_ONLY.iter().copied() { - let rust = squash(&transpile_str(&field_src(kw))); + let src = field_src(kw); + check(&src); + let rust = squash(&transpile_str(&src)); let want = format!("o.{}", rust_member(kw)); assert!( rust.contains(&want), @@ -207,6 +205,20 @@ fn test_extendkw_1_05_every_keyword_is_a_field_name() { } } +#[test] +fn test_extendkw_1_05b_reserved_field_access_compiles_with_rustc() { + // A Ruchy struct cannot declare a keyword-named field yet, so the struct + // is supplied in Rust; the field accesses are the transpiler's output. + let src = "fun probe(o: Widget) -> i32 {\n o.type + o.match\n}\n"; + check(src); + let dir = tempfile::tempdir().expect("tempdir"); + let probe = transpile(dir.path(), src); + let rust = format!( + "struct Widget {{ r#type: i32, r#match: i32 }}\n{probe}\nfn main() {{ println!(\"{{}}\", probe(Widget {{ r#type: 5, r#match: 2 }})); }}\n" + ); + assert_eq!(rustc_run(dir.path(), &rust).trim(), "7", "rust:\n{rust}"); +} + #[test] fn test_extendkw_1_06_keyword_method_chains() { let src = "fun probe(o: Widget) -> i32 {\n o.extend(1).match(2).default().type(3)\n}\n"; diff --git a/tests/letvec_1_growable_locals.rs b/tests/letvec_1_growable_locals.rs new file mode 100644 index 000000000..2a4002fc5 --- /dev/null +++ b/tests/letvec_1_growable_locals.rs @@ -0,0 +1,245 @@ +//! LETVEC-1: an array literal bound by `let` is growable in the interpreter, +//! so the transpiled Rust must be a `Vec` whenever the program needs one. +//! +//! Before the fix `let mut v = [1, 2]; v.push(3)` transpiled to +//! `let mut v = [1, 2]; v.push(3);` and rustc failed with E0599 (no `push` +//! on `[{integer}; 2]`), with or without a `Vec` annotation, in a +//! `fun main` and in a top-level script. +//! +//! The literal becomes `vec![..]` when the binding is annotated `Vec<..>` or +//! is, later in its scope, the receiver of a Vec-only method. A literal that +//! is never grown stays a fixed-size array (`sort`/`reverse` alone work on +//! Rust arrays and do not force a `Vec`). + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn interpret(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + stdout_of(cmd, "ruchy run") +} + +fn transpile(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, "ruchy transpile") +} + +fn transpile_str(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + transpile(dir.path(), src) +} + +/// `ruchy compile` (rustc) the source, then run the binary. +fn compile_and_run(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let bin = dir.join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(60)); + stdout_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "compiled binary") +} + +/// Strip whitespace so `vec ! [1 , 2]` and `vec![1,2]` compare equal. +fn squash(s: &str) -> String { + s.chars().filter(|c| !c.is_whitespace()).collect() +} + +/// The four binding shapes: (label, program) for `op` applied to +/// `[3, 1, 2, 2]`, printing the array and its length afterwards. +fn forms(op: &str) -> Vec<(&'static str, String)> { + let body = |decl: &str, indent: &str| { + format!("{indent}let mut v{decl} = [3, 1, 2, 2]\n{indent}v.{op}\n{indent}println(v)\n{indent}println(v.len())\n") + }; + vec![ + ( + "fun main", + format!("fun main() {{\n{}}}\n", body("", " ")), + ), + ( + "fun main, Vec", + format!("fun main() {{\n{}}}\n", body(": Vec", " ")), + ), + ("script", body("", "")), + ("script, Vec", body(": Vec", "")), + ] +} + +/// Every form transpiles to a `vec![..]` binding, compiles with rustc, and +/// prints `expected` (checked against the interpreter when it supports `op`). +fn assert_grows(op: &str, expected: &str) { + for (label, src) in forms(op) { + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), &src); + assert!( + squash(&rust).contains("vec![3,1,2,2]"), + "{label}, `{op}`: expected a vec![..] binding in:\n{rust}" + ); + let actual = compile_and_run(dir.path(), &src); + assert_eq!(actual, expected, "{label}, `{op}`: rust:\n{rust}"); + } +} + +/// As [`assert_grows`], and the interpreter prints the same thing. +fn assert_grows_like_interpreter(op: &str, expected: &str) { + for (label, src) in forms(op) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!( + interpret(dir.path(), &src), + expected, + "{label}, `{op}`: interpreter" + ); + } + assert_grows(op, expected); +} + +#[test] +fn test_letvec_1_01_push() { + assert_grows_like_interpreter("push(9)", "[3, 1, 2, 2, 9]\n5\n"); +} + +#[test] +fn test_letvec_1_02_pop() { + assert_grows_like_interpreter("pop()", "[3, 1, 2]\n3\n"); +} + +#[test] +fn test_letvec_1_03_extend() { + assert_grows_like_interpreter("extend([7, 8])", "[3, 1, 2, 2, 7, 8]\n6\n"); +} + +#[test] +fn test_letvec_1_04_insert() { + assert_grows_like_interpreter("insert(0, 9)", "[9, 3, 1, 2, 2]\n5\n"); +} + +#[test] +fn test_letvec_1_05_remove() { + assert_grows_like_interpreter("remove(1)", "[3, 2, 2]\n3\n"); +} + +#[test] +fn test_letvec_1_06_clear() { + assert_grows_like_interpreter("clear()", "[]\n0\n"); +} + +#[test] +fn test_letvec_1_07_truncate() { + assert_grows_like_interpreter("truncate(2)", "[3, 1]\n2\n"); +} + +#[test] +fn test_letvec_1_08_dedup() { + assert_grows_like_interpreter("dedup()", "[3, 1, 2]\n3\n"); +} + +#[test] +fn test_letvec_1_09_append_makes_a_vec() { + // `append` is emitted as `push` by call_transpilation.rs, which rustc + // rejects on a Vec; only the binding shape is asserted here. + for (label, src) in forms("append([7, 8])") { + let rust = transpile_str(&src); + assert!( + squash(&rust).contains("vec![3,1,2,2]"), + "{label}: expected a vec![..] binding in:\n{rust}" + ); + } +} + +#[test] +fn test_letvec_1_10_retain() { + assert_grows("retain(|x| *x > 1)", "[3, 2, 2]\n3\n"); +} + +#[test] +fn test_letvec_1_11_drain() { + assert_grows("drain(0..2)", "[2, 2]\n2\n"); +} + +#[test] +fn test_letvec_1_12_resize() { + assert_grows("resize(6, 0)", "[3, 1, 2, 2, 0, 0]\n6\n"); +} + +#[test] +fn test_letvec_1_13_extend_from_slice() { + assert_grows("extend_from_slice(&[7])", "[3, 1, 2, 2, 7]\n5\n"); +} + +#[test] +fn test_letvec_1_14_never_grown_stays_array() { + let src = "fun main() {\n let v = [1, 2, 3]\n println(v.len())\n}\n"; + let rust = squash(&transpile_str(src)); + assert!( + rust.contains("letv=[1,2,3];"), + "expected a fixed array:\n{rust}" + ); + assert!(!rust.contains("vec!"), "unexpected vec!:\n{rust}"); +} + +#[test] +fn test_letvec_1_15_sort_and_reverse_alone_stay_array() { + let src = "fun main() {\n let mut v = [3, 1, 2]\n v.sort()\n v.reverse()\n println(v)\n}\n"; + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), src); + assert!( + squash(&rust).contains("letmutv=[3,1,2];"), + "expected a fixed array:\n{rust}" + ); + assert_eq!(compile_and_run(dir.path(), src), "[3, 2, 1]\n"); +} + +#[test] +fn test_letvec_1_16_only_the_grown_binding_becomes_vec() { + let src = "fun main() {\n let a = [1, 2]\n let mut b = [3]\n b.push(4)\n println(a.len() + b.len())\n}\n"; + let dir = tempfile::tempdir().expect("tempdir"); + let rust = squash(&transpile(dir.path(), src)); + assert!(rust.contains("leta=[1,2];"), "a stays an array:\n{rust}"); + assert!( + rust.contains("letmutb=vec![3];"), + "b becomes a vec:\n{rust}" + ); + assert_eq!(compile_and_run(dir.path(), src), "4\n"); +} + +#[test] +fn test_letvec_1_17_growth_inside_nested_block() { + let src = "fun main() {\n let mut v = [1]\n if true {\n for i in 0..3 {\n v.push(i)\n }\n }\n println(v.len())\n}\n"; + let dir = tempfile::tempdir().expect("tempdir"); + let expected = interpret(dir.path(), src); + assert_eq!(expected, "4\n"); + assert_eq!(compile_and_run(dir.path(), src), expected); +} From 5be9303845e64461edd4670b42ce55a1695c1551 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 08:05:42 +0200 Subject: [PATCH 052/106] RESFIELD-1 (GREEN), LETVEC-1 (partial): r#kw field names; Vec-annotated array literals - field_access.rs, collections.rs (struct literal field names), types.rs (struct field declarations, Default impl): Self::safe_ident instead of format_ident!, so o.type / o.match emit o.r#type / o.r#match - mutation_detection.rs: VEC_ONLY_METHODS + is_grown_as_vec (AST walk via any_expr: the variable is the receiver of a Vec-only method) - bindings.rs: growable_list_tokens: a non-empty array literal bound by let becomes vec![..] when annotated Vec<..> or grown in the let body - extendkw 05b: rustc driver calls transpiled main via make_widget() LETVEC-1 unannotated statement-level lets (Let with a Unit body followed by sibling statements) are not covered: the siblings are only visible in body_generation.rs / block_transpiler.rs, outside this ticket's scope. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 217 ++++++++++++------ src/backend/transpiler/bindings.rs | 37 +++ .../expressions_helpers/collections.rs | 8 +- .../expressions_helpers/field_access.rs | 16 +- src/backend/transpiler/mutation_detection.rs | 37 +++ src/backend/transpiler/types.rs | 4 +- tests/extendkw_1_keyword_methods.rs | 7 +- 7 files changed, 236 insertions(+), 90 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 5e6fbc7d6..5f9e4fac2 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T05:58:22.268670176Z", + "created_at": "2026-09-24T06:15:16.669402560Z", "git_context": null, "files": { "./build.rs": { @@ -17776,48 +17776,48 @@ }, "./src/frontend/parser/functions.rs": { "content_hash": [ - 95, - 144, - 172, - 186, - 131, - 244, - 161, - 180, - 212, - 137, - 122, - 1, - 128, - 217, - 4, - 253, - 10, - 231, - 137, - 124, + 115, + 116, + 251, + 31, + 224, + 196, + 142, + 62, + 65, + 222, + 189, 246, - 254, + 100, + 54, + 102, + 12, 145, - 94, - 168, - 3, - 51, - 57, + 241, + 130, + 104, + 223, + 249, + 65, + 6, + 58, + 115, + 109, + 246, + 16, 26, - 31, - 186, - 250 + 126, + 86 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.897436, + "duplication_ratio": 15.962644, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.89744, + "total": 75.96265, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -17829,15 +17829,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 104 duplicate code patterns" + "issue": "Found 102 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.1025643, + "amount": 4.0373564, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.5%" + "issue": "Code duplication: 20.2%" }, { "source_metric": "StructuralComplexity", @@ -17845,7 +17845,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 82" + "issue": "High cognitive complexity: 84" }, { "source_metric": "StructuralComplexity", @@ -17853,7 +17853,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 85" + "issue": "High cyclomatic complexity: 81" } ], "critical_defects_count": 0, @@ -18138,50 +18138,121 @@ }, "git_context": null }, - "./src/frontend/parser/mod.rs": { + "./src/frontend/parser/member_names.rs": { "content_hash": [ - 116, + 255, + 59, + 230, + 41, + 92, + 168, + 24, + 189, + 199, + 70, + 26, + 34, + 126, + 48, + 31, + 190, 45, - 232, + 124, + 135, + 131, + 244, + 54, + 4, + 240, + 87, + 32, + 76, + 249, + 118, + 104, + 174, + 84 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 8.0, + "total": 98.18182, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/frontend/parser/member_names.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 2.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 4 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, + "./src/frontend/parser/mod.rs": { + "content_hash": [ 201, - 218, - 233, - 237, - 227, 182, - 70, - 124, - 38, - 250, - 207, - 92, - 155, 251, - 134, - 55, - 134, - 132, - 27, - 154, - 209, - 110, - 250, - 133, - 173, - 105, - 210, - 132, - 26 + 234, + 120, + 197, + 152, + 66, + 58, + 126, + 226, + 108, + 153, + 21, + 215, + 176, + 232, + 14, + 244, + 159, + 158, + 93, + 86, + 119, + 122, + 247, + 198, + 223, + 199, + 230, + 191, + 130 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.042398, + "duplication_ratio": 17.045454, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.0424, + "total": 77.045456, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -18197,7 +18268,7 @@ }, { "source_metric": "Duplication", - "amount": 2.957601, + "amount": 2.9545453, "applied_to": [ "Duplication" ], @@ -44974,10 +45045,10 @@ } }, "summary": { - "total_files": 529, - "avg_score": 90.30044, + "total_files": 530, + "avg_score": 90.31544, "grade_distribution": { - "A+": 194, + "A+": 195, "A": 176, "A-": 33, "B+": 29, @@ -44989,7 +45060,7 @@ "languages": { "JavaScript": 14, "Python": 4, - "Rust": 507, + "Rust": 508, "TypeScript": 4 } } diff --git a/src/backend/transpiler/bindings.rs b/src/backend/transpiler/bindings.rs index dcd984d1f..a9024101e 100644 --- a/src/backend/transpiler/bindings.rs +++ b/src/backend/transpiler/bindings.rs @@ -87,6 +87,7 @@ impl Transpiler { ExprKind::List(items) if items.is_empty() => (self.transpile_expr(value)?, true), _ => (self.transpile_expr(value)?, false), }; + let value_tokens = Self::growable_list_tokens(name, value, None, body, value_tokens); // HOTFIX: If body is Unit, this is a top-level let statement without scoping if matches!(body.kind, ExprKind::Literal(Literal::Unit)) { @@ -287,6 +288,11 @@ impl Transpiler { // Handle value tokens and type hints let (value_tokens, needs_vec_type_hint) = self.process_let_value_with_type(name, value, type_annotation, is_mutable_var)?; + let value_tokens = if is_const { + value_tokens + } else { + Self::growable_list_tokens(name, value, type_annotation, body, value_tokens) + }; // Generate type annotation let type_tokens = self.generate_type_tokens(type_annotation, needs_vec_type_hint)?; @@ -307,6 +313,29 @@ impl Transpiler { } } + /// LETVEC-1: a non-empty array literal bound by `let` becomes `vec![..]` + /// when the binding is annotated `Vec<..>` or is grown later in `body` + /// (the receiver of a Vec-only method). Otherwise `tokens` is returned + /// unchanged, so a never-grown literal stays a fixed-size array. + fn growable_list_tokens( + name: &str, + value: &Expr, + type_annotation: Option<&Type>, + body: &Expr, + tokens: TokenStream, + ) -> TokenStream { + let is_literal = matches!(&value.kind, ExprKind::List(items) if !items.is_empty()); + let text = tokens.to_string(); + let is_array_tokens = text.starts_with('[') && text.ends_with(']'); + let needs_vec = type_annotation.is_some_and(is_vec_annotation) + || super::mutation_detection::is_grown_as_vec(name, body); + if is_literal && is_array_tokens && needs_vec { + quote! { vec! #tokens } + } else { + tokens + } + } + /// Process value for let-with-type, handling string/list conversions fn process_let_value_with_type( &self, @@ -509,6 +538,14 @@ impl Transpiler { } } +/// LETVEC-1: `Vec<..>` annotation (a bare `Vec` or a generic `Vec`). +fn is_vec_annotation(ty: &Type) -> bool { + match &ty.kind { + TypeKind::Generic { base, .. } | TypeKind::Named(base) => base == "Vec", + _ => false, + } +} + // ============================================================================ // Tests // ============================================================================ diff --git a/src/backend/transpiler/expressions_helpers/collections.rs b/src/backend/transpiler/expressions_helpers/collections.rs index c0abce446..868331d28 100644 --- a/src/backend/transpiler/expressions_helpers/collections.rs +++ b/src/backend/transpiler/expressions_helpers/collections.rs @@ -246,10 +246,10 @@ impl Transpiler { // BOOK-COMPAT-007B: Check for auto-boxed recursive types let auto_boxed = self.auto_boxed_fields.borrow(); for (field_name, value) in fields { - let field_ident = format_ident!("{}", field_name); - // BOOK-COMPAT-002 FIX: Add .to_string() for String fields with string literals - // When a struct field is typed as String and the value is a string literal, - // we need to add .to_string() for the Rust code to compile correctly. + let field_ident = Self::safe_ident(field_name); // RESFIELD-1 + // BOOK-COMPAT-002 FIX: Add .to_string() for String fields with string literals + // When a struct field is typed as String and the value is a string literal, + // we need to add .to_string() for the Rust code to compile correctly. let field_type = field_types.get(&(base_struct_name.to_string(), field_name.clone())); let needs_to_string = matches!(field_type, Some(t) if t == "String") && matches!(&value.kind, ExprKind::Literal(Literal::String(_))); diff --git a/src/backend/transpiler/expressions_helpers/field_access.rs b/src/backend/transpiler/expressions_helpers/field_access.rs index d55cbc6de..d2c235391 100644 --- a/src/backend/transpiler/expressions_helpers/field_access.rs +++ b/src/backend/transpiler/expressions_helpers/field_access.rs @@ -4,7 +4,7 @@ use super::super::Transpiler; use crate::frontend::ast::Expr; use anyhow::Result; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; impl Transpiler { /// Check if an expression represents a module path (like `std::time`) @@ -147,7 +147,7 @@ impl Transpiler { let known_methods = [ "success", "exists", "is_empty", "is_some", "is_none", "is_ok", "is_err", ]; - let field_ident = format_ident!("{}", field); + let field_ident = Self::safe_ident(field); // RESFIELD-1 if known_methods.contains(&field) { // Known method - use . and add () @@ -167,32 +167,32 @@ impl Transpiler { } ExprKind::Identifier(name) if name.contains("::") => { // Module path identifier - use :: syntax - let field_ident = format_ident!("{}", field); + let field_ident = Self::safe_ident(field); // RESFIELD-1 Ok(quote! { #obj_tokens::#field_ident }) } ExprKind::Identifier(name) if name == "std" => { // STDLIB-003: std module - use :: syntax for std::time, std::fs, etc. - let field_ident = format_ident!("{}", field); + let field_ident = Self::safe_ident(field); // RESFIELD-1 Ok(quote! { #obj_tokens::#field_ident }) } ExprKind::Identifier(name) if self.module_names.contains(name) => { // ISSUE-103: Module name - use :: syntax for module::function() // Examples: helper::get_message(), logger::log_info() - let field_ident = format_ident!("{}", field); + let field_ident = Self::safe_ident(field); // RESFIELD-1 Ok(quote! { #obj_tokens::#field_ident }) } ExprKind::Identifier(name) if name.chars().next().is_some_and(char::is_uppercase) => { // TRANSPILER-065: Type name (PascalCase) - use :: for associated functions/constructors // Examples: String::from(), Result::Ok(), Vec::new() // Heuristic: Rust types start with uppercase, instances with lowercase - let field_ident = format_ident!("{}", field); + let field_ident = Self::safe_ident(field); // RESFIELD-1 Ok(quote! { #obj_tokens::#field_ident }) } ExprKind::Identifier(name) if Self::is_module_like_identifier(name) => { // PARSER-094: Module-like identifier (lowercase_underscore pattern) // Examples: http_client::http_get(), my_module::function() // Issue #137: Fixes ruchy-lambda AWS Lambda runtime module calls - let field_ident = format_ident!("{}", field); + let field_ident = Self::safe_ident(field); // RESFIELD-1 Ok(quote! { #obj_tokens::#field_ident }) } _ => { @@ -210,7 +210,7 @@ impl Transpiler { let known_methods = [ "success", "exists", "is_empty", "is_some", "is_none", "is_ok", "is_err", ]; - let field_ident = format_ident!("{}", field); + let field_ident = Self::safe_ident(field); // RESFIELD-1 if known_methods.contains(&field) { // Known method - add () for method call diff --git a/src/backend/transpiler/mutation_detection.rs b/src/backend/transpiler/mutation_detection.rs index 1142d7d29..7aaebfd91 100644 --- a/src/backend/transpiler/mutation_detection.rs +++ b/src/backend/transpiler/mutation_detection.rs @@ -16,6 +16,43 @@ pub const MUTATING_STD_METHODS: &[&str] = &[ "reverse", "dedup", "retain", "drain", "push_str", "swap", "entry", ]; +/// LETVEC-1: methods that exist on `Vec` but not on a fixed-size array. A +/// `let`-bound array literal that is the receiver of one of these must be +/// emitted as `vec![..]`. `sort`/`reverse` work on arrays and are not listed. +pub const VEC_ONLY_METHODS: &[&str] = &[ + "push", + "pop", + "extend", + "insert", + "remove", + "clear", + "truncate", + "append", + "dedup", + "retain", + "drain", + "resize", + "extend_from_slice", +]; + +/// LETVEC-1: true when the variable `name` is the receiver of a +/// [`VEC_ONLY_METHODS`] call anywhere in `expr`. +pub fn is_grown_as_vec(name: &str, expr: &Expr) -> bool { + any_expr(expr, &|e| is_vec_only_call_on(name, e)) +} + +/// `name.(..)` with `m` a [`VEC_ONLY_METHODS`] method. +fn is_vec_only_call_on(name: &str, expr: &Expr) -> bool { + let ExprKind::MethodCall { + receiver, method, .. + } = &expr.kind + else { + return false; + }; + matches!(&receiver.kind, ExprKind::Identifier(root) if root == name) + && VEC_ONLY_METHODS.contains(&method.as_str()) +} + /// Checks if a variable is mutated (reassigned or modified) in an expression tree /// /// This function traverses the AST recursively to detect writes through a diff --git a/src/backend/transpiler/types.rs b/src/backend/transpiler/types.rs index 5cbc6f92a..ed75f7e02 100644 --- a/src/backend/transpiler/types.rs +++ b/src/backend/transpiler/types.rs @@ -488,7 +488,7 @@ impl Transpiler { let field_tokens: Vec = fields .iter() .map(|field| { - let field_name = format_ident!("{}", field.name); + let field_name = Self::safe_ident(&field.name); // RESFIELD-1 // BOOK-COMPAT-001: Add lifetime to reference types if needed let field_type = if needs_lifetime { @@ -567,7 +567,7 @@ impl Transpiler { let default_field_tokens: Result> = fields .iter() .map(|field| -> Result { - let field_name = format_ident!("{}", field.name); + let field_name = Self::safe_ident(&field.name); // RESFIELD-1 if let Some(ref default_expr) = field.default_value { let default_value = self.transpile_expr(default_expr)?; // BOOK-COMPAT-004: Add .to_string() for String fields with string literal defaults diff --git a/tests/extendkw_1_keyword_methods.rs b/tests/extendkw_1_keyword_methods.rs index 522ca36f6..7cf2bc98e 100644 --- a/tests/extendkw_1_keyword_methods.rs +++ b/tests/extendkw_1_keyword_methods.rs @@ -208,13 +208,14 @@ fn test_extendkw_1_05_every_keyword_is_a_field_name() { #[test] fn test_extendkw_1_05b_reserved_field_access_compiles_with_rustc() { // A Ruchy struct cannot declare a keyword-named field yet, so the struct - // is supplied in Rust; the field accesses are the transpiler's output. - let src = "fun probe(o: Widget) -> i32 {\n o.type + o.match\n}\n"; + // and its constructor are supplied in Rust; `probe` and `main` are the + // transpiler's output. + let src = "fun probe(o: Widget) -> i32 {\n o.type + o.match\n}\n\nfun main() {\n println(probe(make_widget()))\n}\n"; check(src); let dir = tempfile::tempdir().expect("tempdir"); let probe = transpile(dir.path(), src); let rust = format!( - "struct Widget {{ r#type: i32, r#match: i32 }}\n{probe}\nfn main() {{ println!(\"{{}}\", probe(Widget {{ r#type: 5, r#match: 2 }})); }}\n" + "struct Widget {{ r#type: i32, r#match: i32 }}\nfn make_widget() -> Widget {{ Widget {{ r#type: 5, r#match: 2 }} }}\n{probe}" ); assert_eq!(rustc_run(dir.path(), &rust).trim(), "7", "rust:\n{rust}"); } From 071dc9e1ef2193c9690c6dcb4edf75de4d9d62e0 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 08:14:26 +0200 Subject: [PATCH 053/106] LETVEC-1 (GREEN), RESFIELD-1: growable statement-level lets; keyword field declarations - bindings.rs: transpile_block_statement / grown_let_as_vec: a statement-level let (Unit body) whose array literal is grown by a later sibling statement (is_grown_as_vec) is transpiled with the literal as vec![..] - body_generation.rs, block_transpiler.rs: block loops call it with the following siblings - call_transpilation.rs: Ruchy append(x) -> Rust extend(x) (was push) - parser structs.rs / types.rs: a keyword token (member_names table) is a field name in struct declarations and struct literals - eval_array.rs: in-place resize(n, x) and extend_from_slice(x) - tests: letvec append/resize/extend_from_slice differential; retain/drain stay rustc-only (not interpreter array methods); RESFIELD rustc test uses a Ruchy-declared struct with type/match fields Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 348 +++++++++--------- src/backend/transpiler/bindings.rs | 46 +++ src/backend/transpiler/block_transpiler.rs | 4 +- src/backend/transpiler/body_generation.rs | 2 +- src/backend/transpiler/call_transpilation.rs | 7 +- .../parser/expressions_helpers/structs.rs | 17 +- src/frontend/parser/types.rs | 17 +- src/runtime/eval_array.rs | 14 +- tests/extendkw_1_keyword_methods.rs | 11 +- tests/letvec_1_growable_locals.rs | 17 +- 10 files changed, 267 insertions(+), 216 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 5f9e4fac2..b1d07b041 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T06:15:16.669402560Z", + "created_at": "2026-09-24T06:15:17.732523048Z", "git_context": null, "files": { "./build.rs": { @@ -3040,49 +3040,49 @@ }, "./src/backend/transpiler/bindings.rs": { "content_hash": [ - 35, - 10, - 211, + 197, + 185, + 36, + 78, + 237, + 200, + 139, 29, - 167, - 63, - 28, - 100, - 228, - 228, - 60, - 122, - 28, - 74, + 206, + 234, + 3, + 169, + 117, + 133, + 134, + 238, + 231, + 202, + 135, 67, - 215, - 36, - 179, - 157, + 149, 66, - 161, - 226, - 22, - 238, - 170, - 111, - 86, - 167, - 174, - 130, - 58, - 254 + 105, + 105, + 141, + 10, + 247, + 163, + 24, + 64, + 96, + 162 ], "score": { - "structural_complexity": 8.0, + "structural_complexity": 5.0999994, "semantic_complexity": 20.0, - "duplication_ratio": 13.7076025, + "duplication_ratio": 13.807909, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 81.7076, - "grade": "B+", + "total": 78.907906, + "grade": "B", "confidence": 1.0, "language": "Rust", "file_path": "./src/backend/transpiler/bindings.rs", @@ -3093,31 +3093,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 96 duplicate code patterns" + "issue": "Found 97 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.2923975, + "amount": 6.1920905, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 31.5%" + "issue": "Code duplication: 31.0%" }, { "source_metric": "StructuralComplexity", - "amount": 9.0, + "amount": 9.900001, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 45" + "issue": "High cognitive complexity: 48" }, { "source_metric": "StructuralComplexity", - "amount": 8.0, + "amount": 10.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 46" + "issue": "High cyclomatic complexity: 50" } ], "critical_defects_count": 0, @@ -5665,38 +5665,38 @@ }, "./src/backend/transpiler/expressions_helpers/collections.rs": { "content_hash": [ - 51, - 111, - 72, - 184, - 51, - 77, - 188, - 94, - 55, - 114, - 85, - 245, - 71, + 49, + 36, 177, - 230, + 54, + 138, + 196, + 222, + 226, + 36, + 24, + 246, + 28, + 246, + 185, + 104, + 114, + 233, + 118, + 160, + 157, + 157, + 162, + 87, + 52, + 227, + 188, + 238, + 84, 31, - 241, - 194, - 64, - 213, - 149, - 235, - 245, - 19, - 103, - 139, - 100, - 29, - 126, - 119, - 248, - 173 + 15, + 1, + 43 ], "score": { "structural_complexity": 16.599998, @@ -5752,38 +5752,38 @@ }, "./src/backend/transpiler/expressions_helpers/field_access.rs": { "content_hash": [ - 12, - 216, - 108, - 34, + 20, + 242, + 249, 212, - 199, - 54, - 94, - 220, - 247, - 184, - 248, - 2, - 78, - 175, - 208, - 27, - 254, - 121, - 214, - 154, - 0, - 31, - 102, - 108, - 37, - 235, - 55, + 57, + 120, + 103, + 23, + 201, + 74, + 24, + 232, + 151, + 218, + 64, + 5, + 243, + 165, + 221, 18, - 203, - 150, - 25 + 97, + 209, + 57, + 9, + 78, + 34, + 145, + 201, + 210, + 233, + 228, + 205 ], "score": { "structural_complexity": 16.3, @@ -7220,48 +7220,48 @@ }, "./src/backend/transpiler/mutation_detection.rs": { "content_hash": [ - 55, - 50, - 59, - 177, - 155, - 129, - 81, - 250, - 53, - 180, - 50, - 205, - 197, - 144, - 49, 9, - 142, - 66, - 205, - 135, - 79, - 40, - 127, - 99, - 36, - 151, - 161, - 199, - 104, - 106, - 179, - 249 + 166, + 164, + 187, + 134, + 75, + 93, + 42, + 245, + 213, + 82, + 97, + 234, + 158, + 175, + 226, + 42, + 62, + 21, + 176, + 63, + 0, + 119, + 186, + 108, + 198, + 88, + 181, + 30, + 222, + 27, + 88 ], "score": { "structural_complexity": 17.2, "semantic_complexity": 20.0, - "duplication_ratio": 17.072136, + "duplication_ratio": 17.07351, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 94.27214, + "total": 94.27351, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -7273,11 +7273,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 109 duplicate code patterns" + "issue": "Found 114 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.927864, + "amount": 2.9264908, "applied_to": [ "Duplication" ], @@ -8818,48 +8818,48 @@ }, "./src/backend/transpiler/types.rs": { "content_hash": [ - 236, - 232, - 220, - 103, - 69, - 244, - 15, - 237, - 89, - 169, - 21, - 161, 252, - 119, - 122, - 100, - 183, - 25, - 155, - 154, - 111, + 106, + 12, + 56, + 170, + 103, + 11, + 58, + 94, + 223, + 137, + 186, + 175, + 128, + 50, + 177, + 94, + 40, 27, - 228, - 178, - 249, - 30, - 229, - 52, - 43, - 38, - 231, - 100 + 64, + 82, + 201, + 216, + 94, + 170, + 0, + 173, + 168, + 171, + 221, + 172, + 151 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.679611, + "duplication_ratio": 15.695793, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.67961, + "total": 75.69579, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -8879,15 +8879,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 133 duplicate code patterns" + "issue": "Found 134 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.3203883, + "amount": 4.3042073, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 21.6%" + "issue": "Code duplication: 21.5%" }, { "source_metric": "StructuralComplexity", @@ -45046,13 +45046,13 @@ }, "summary": { "total_files": 530, - "avg_score": 90.31544, + "avg_score": 90.310196, "grade_distribution": { "A+": 195, "A": 176, "A-": 33, - "B+": 29, - "B": 68, + "B+": 28, + "B": 69, "B-": 26, "C": 1, "F": 2 diff --git a/src/backend/transpiler/bindings.rs b/src/backend/transpiler/bindings.rs index a9024101e..27a8a30ae 100644 --- a/src/backend/transpiler/bindings.rs +++ b/src/backend/transpiler/bindings.rs @@ -336,6 +336,20 @@ impl Transpiler { } } + /// LETVEC-1: transpile one statement of a block. A statement-level `let` + /// (Unit body) whose scope is the following siblings `rest` gets its + /// array literal emitted as `vec![..]` when a sibling grows the binding. + pub(crate) fn transpile_block_statement( + &self, + expr: &Expr, + rest: &[Expr], + ) -> Result { + match grown_let_as_vec(expr, rest) { + Some(rewritten) => self.transpile_expr(&rewritten), + None => self.transpile_expr(expr), + } + } + /// Process value for let-with-type, handling string/list conversions fn process_let_value_with_type( &self, @@ -538,6 +552,38 @@ impl Transpiler { } } +/// LETVEC-1: a copy of the statement-level `let` `expr` with its non-empty +/// array literal replaced by `vec![..]`, when a later sibling in `rest` is a +/// Vec-only method call on the binding; `None` otherwise. +fn grown_let_as_vec(expr: &Expr, rest: &[Expr]) -> Option { + let ExprKind::Let { + name, value, body, .. + } = &expr.kind + else { + return None; + }; + let ExprKind::List(items) = &value.kind else { + return None; + }; + let is_statement = matches!(body.kind, ExprKind::Literal(Literal::Unit)); + let grown = rest + .iter() + .any(|e| super::mutation_detection::is_grown_as_vec(name, e)); + if items.is_empty() || !is_statement || !grown { + return None; + } + let mut vec_value = (**value).clone(); + vec_value.kind = ExprKind::MacroInvocation { + name: "vec".to_string(), + args: items.clone(), + }; + let mut rewritten = expr.clone(); + if let ExprKind::Let { value, .. } = &mut rewritten.kind { + **value = vec_value; + } + Some(rewritten) +} + /// LETVEC-1: `Vec<..>` annotation (a bare `Vec` or a generic `Vec`). fn is_vec_annotation(ty: &Type) -> bool { match &ty.kind { diff --git a/src/backend/transpiler/block_transpiler.rs b/src/backend/transpiler/block_transpiler.rs index e3910086e..917d5af2a 100644 --- a/src/backend/transpiler/block_transpiler.rs +++ b/src/backend/transpiler/block_transpiler.rs @@ -29,7 +29,7 @@ impl Transpiler { let mut statements = Vec::new(); for (i, expr) in exprs.iter().enumerate() { - let expr_tokens = self.transpile_expr(expr)?; + let expr_tokens = self.transpile_block_statement(expr, &exprs[i + 1..])?; let is_last = i == exprs.len() - 1; let is_let = Self::is_let_expr(expr); @@ -109,7 +109,7 @@ impl Transpiler { let mut statements = Vec::new(); for (i, expr) in exprs.iter().enumerate() { - let expr_tokens = self.transpile_expr(expr)?; + let expr_tokens = self.transpile_block_statement(expr, &exprs[i + 1..])?; let is_last = i == exprs.len() - 1; let is_let = Self::is_let_expr(expr); diff --git a/src/backend/transpiler/body_generation.rs b/src/backend/transpiler/body_generation.rs index 1d5900278..f05c01014 100644 --- a/src/backend/transpiler/body_generation.rs +++ b/src/backend/transpiler/body_generation.rs @@ -70,7 +70,7 @@ impl Transpiler { let mut statements = Vec::new(); for (i, expr) in exprs.iter().enumerate() { - let expr_tokens = self.transpile_expr(expr)?; + let expr_tokens = self.transpile_block_statement(expr, &exprs[i + 1..])?; let is_let = Self::is_let_expression(expr); if i < exprs.len() - 1 { diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index c4d9d5fb1..d86fde493 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -469,7 +469,8 @@ impl Transpiler { | "substring" | "strip" | "lstrip" | "rstrip" | "startswith" | "endswith" | "split" | "replace" => self.transpile_string_methods(obj_tokens, method, arg_tokens), // List methods - "append" => Ok(quote! { #obj_tokens.push(#(#arg_tokens),*) }), + // LETVEC-1: Ruchy append(x) adds x's elements (by value), like extend + "append" => Ok(quote! { #obj_tokens.extend(#(#arg_tokens),*) }), "extend" => Ok(quote! { #obj_tokens.extend(#(#arg_tokens),*) }), // Collection methods "push" | "pop" | "contains" => { @@ -648,7 +649,7 @@ mod tests { } #[test] - fn test_dispatch_append_to_push() { + fn test_dispatch_append_to_extend() { let transpiler = make_transpiler(); let obj_tokens = quote! { vec }; let arg_tokens = vec![quote! { 42 }]; @@ -657,7 +658,7 @@ mod tests { let result = transpiler .dispatch_method_by_category(&obj_tokens, "append", &method_ident, &arg_tokens, &object) .unwrap(); - assert!(result.to_string().contains("push")); + assert!(result.to_string().contains("extend")); } #[test] diff --git a/src/frontend/parser/expressions_helpers/structs.rs b/src/frontend/parser/expressions_helpers/structs.rs index 04ad4c4fa..de15a5339 100644 --- a/src/frontend/parser/expressions_helpers/structs.rs +++ b/src/frontend/parser/expressions_helpers/structs.rs @@ -227,12 +227,17 @@ fn parse_private_keyword(state: &mut ParserState) { pub(in crate::frontend::parser) fn parse_single_struct_field( state: &mut ParserState, ) -> Result<(String, Type, Option)> { - let field_name = if let Some((Token::Identifier(n), _)) = state.tokens.peek() { - let name = n.clone(); - state.tokens.advance(); - name - } else { - bail!("Expected field name"); + // RESFIELD-1: a keyword is a field name too (`type: i32`), as after '.' + let field_name = match state + .tokens + .peek() + .and_then(|(token, _)| crate::frontend::parser::member_names::member_name(token)) + { + Some(name) => { + state.tokens.advance(); + name + } + None => bail!("Expected field name"), }; state.tokens.expect(&Token::Colon)?; diff --git a/src/frontend/parser/types.rs b/src/frontend/parser/types.rs index 6abbb7377..25c9ed739 100644 --- a/src/frontend/parser/types.rs +++ b/src/frontend/parser/types.rs @@ -17,13 +17,18 @@ fn parse_struct_base(state: &mut ParserState) -> Result>> { } // Helper: Parse field name identifier (complexity: 2) +// RESFIELD-1: a keyword is a field name in a struct literal too (`type: 5`) fn parse_field_name(state: &mut ParserState) -> Result { - if let Some((Token::Identifier(name), _)) = state.tokens.peek() { - let name = name.clone(); - state.tokens.advance(); - Ok(name) - } else { - bail!("Expected field name"); + let name = state + .tokens + .peek() + .and_then(|(token, _)| crate::frontend::parser::member_names::member_name(token)); + match name { + Some(name) => { + state.tokens.advance(); + Ok(name) + } + None => bail!("Expected field name"), } } diff --git a/src/runtime/eval_array.rs b/src/runtime/eval_array.rs index 78816c1f5..f144d5923 100644 --- a/src/runtime/eval_array.rs +++ b/src/runtime/eval_array.rs @@ -119,9 +119,11 @@ where pub(crate) fn is_in_place_array_method(method: &str, arg_count: usize) -> bool { matches!( (method, arg_count), - ("push" | "append" | "extend" | "remove" | "truncate", 1) - | ("pop" | "sort" | "reverse" | "clear" | "dedup", 0) - | ("insert", 2) + ( + "push" | "append" | "extend" | "extend_from_slice" | "remove" | "truncate", + 1 + ) | ("pop" | "sort" | "reverse" | "clear" | "dedup", 0) + | ("insert" | "resize", 2) ) } @@ -169,10 +171,14 @@ fn apply_in_place_with_args( ) -> Result { match (method, args) { ("push", [item]) => items.push(item.clone()), - ("append" | "extend", [other]) => items.extend(iterable_items(method, other)?), + ("append" | "extend" | "extend_from_slice", [other]) => { + items.extend(iterable_items(method, other)?); + } ("remove", [index]) => return remove_at(items, index), ("truncate", [len]) => items.truncate(index_arg("truncate", len)?), ("insert", [index, item]) => insert_at(items, index, item)?, + // LETVEC-1: resize(n, x) pads with clones of x or truncates to n + ("resize", [len, item]) => items.resize(index_arg("resize", len)?, item.clone()), _ => return Err(unknown_in_place(method)), } Ok(Value::Nil) diff --git a/tests/extendkw_1_keyword_methods.rs b/tests/extendkw_1_keyword_methods.rs index 7cf2bc98e..fc5dc75fc 100644 --- a/tests/extendkw_1_keyword_methods.rs +++ b/tests/extendkw_1_keyword_methods.rs @@ -207,16 +207,11 @@ fn test_extendkw_1_05_every_keyword_is_a_field_name() { #[test] fn test_extendkw_1_05b_reserved_field_access_compiles_with_rustc() { - // A Ruchy struct cannot declare a keyword-named field yet, so the struct - // and its constructor are supplied in Rust; `probe` and `main` are the - // transpiler's output. - let src = "fun probe(o: Widget) -> i32 {\n o.type + o.match\n}\n\nfun main() {\n println(probe(make_widget()))\n}\n"; + // RESFIELD-1: keyword field names in a declaration, a literal and an access. + let src = "struct Widget {\n type: i32,\n match: i32,\n}\n\nfun probe(o: Widget) -> i32 {\n o.type + o.match\n}\n\nfun main() {\n println(probe(Widget { type: 5, match: 2 }))\n}\n"; check(src); let dir = tempfile::tempdir().expect("tempdir"); - let probe = transpile(dir.path(), src); - let rust = format!( - "struct Widget {{ r#type: i32, r#match: i32 }}\nfn make_widget() -> Widget {{ Widget {{ r#type: 5, r#match: 2 }} }}\n{probe}" - ); + let rust = transpile(dir.path(), src); assert_eq!(rustc_run(dir.path(), &rust).trim(), "7", "rust:\n{rust}"); } diff --git a/tests/letvec_1_growable_locals.rs b/tests/letvec_1_growable_locals.rs index 2a4002fc5..7bc3016a6 100644 --- a/tests/letvec_1_growable_locals.rs +++ b/tests/letvec_1_growable_locals.rs @@ -167,20 +167,13 @@ fn test_letvec_1_08_dedup() { } #[test] -fn test_letvec_1_09_append_makes_a_vec() { - // `append` is emitted as `push` by call_transpilation.rs, which rustc - // rejects on a Vec; only the binding shape is asserted here. - for (label, src) in forms("append([7, 8])") { - let rust = transpile_str(&src); - assert!( - squash(&rust).contains("vec![3,1,2,2]"), - "{label}: expected a vec![..] binding in:\n{rust}" - ); - } +fn test_letvec_1_09_append() { + assert_grows_like_interpreter("append([7, 8])", "[3, 1, 2, 2, 7, 8]\n6\n"); } #[test] fn test_letvec_1_10_retain() { + // retain/drain are not interpreter array methods: rustc output only. assert_grows("retain(|x| *x > 1)", "[3, 2, 2]\n3\n"); } @@ -191,12 +184,12 @@ fn test_letvec_1_11_drain() { #[test] fn test_letvec_1_12_resize() { - assert_grows("resize(6, 0)", "[3, 1, 2, 2, 0, 0]\n6\n"); + assert_grows_like_interpreter("resize(6, 0)", "[3, 1, 2, 2, 0, 0]\n6\n"); } #[test] fn test_letvec_1_13_extend_from_slice() { - assert_grows("extend_from_slice(&[7])", "[3, 1, 2, 2, 7]\n5\n"); + assert_grows_like_interpreter("extend_from_slice(&[7])", "[3, 1, 2, 2, 7]\n5\n"); } #[test] From 8b483a0864bb73afe2a5fe95f95b689382e46df9 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 08:15:50 +0200 Subject: [PATCH 054/106] RHLGA-1: CHANGELOG and roadmap for the G2b interpreter/transpiler fixes Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 18 ++++++++++++++++++ docs/roadmaps/roadmap.yaml | 14 +++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4114266e..9945a0c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,24 @@ Behaviour changes a program can observe are marked **(behaviour)**. `unwrap`/`expect` on an absent value is an error naming it, and so is `unwrap_or_default`, whose default type is unknown at runtime. A plain object has `get(key)`, returning the value or nil. +- **(behaviour)** In-place array methods change their receiver, as in Rust + (FIELDPOP-1, ARRAYMUT-1). `v.sort()`, `reverse`, `append`, `extend`, + `insert`, `remove`, `clear`, `truncate`, `dedup`, `resize` and + `extend_from_slice` mutate a local array or a field (`self.items.pop()`) + and return what Rust returns: nil, the removed element for `remove`, and the + element or nil for `pop`. Before, `sort` and `reverse` returned a copy and + left the variable unchanged, a field's `push`/`pop` was lost, and `insert`, + `remove`, `clear` and `truncate` were unknown. `sorted`/`reversed` still + return a copy. `a.append(b)` does not empty `b`. +- A `let`-bound array literal that is later grown (`push`, `extend`, `insert`, + …) or annotated `Vec<…>` transpiles to `vec![…]`, and so does an array + literal for a `Vec<…>` struct field (LETVEC-1, VECLIT-1). Before, + `let mut v = [1, 2]; v.push(3)` failed rustc (E0599). `append` transpiles to + `extend`. +- A keyword can name a method or field after `.` or `?.` (`v.extend(x)`, + `o.type`), and a struct can declare it (`struct W { type: i32 }`). A Rust + reserved word is emitted as `r#type` (EXTENDKW-1, RESFIELD-1). +- Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; - they write compiled output (`a.out`, `.rlib`) into temp dirs, never the repository; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 103e3c8be..27c54eb83 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4043,7 +4043,7 @@ roadmap: github_issue: null item_type: task title: 'FIELDPOP-1: self.items.pop() inside a method returns the whole array instead of the popped element (interpreter)' - status: inprogress + status: completed priority: critical assigned_to: null created: 2026-09-24T05:31:58Z @@ -4067,7 +4067,7 @@ roadmap: github_issue: null item_type: task title: 'VECLIT-1: an array literal assigned to a Vec field transpiles to an array, rustc E0308' - status: inprogress + status: completed priority: high assigned_to: null created: 2026-09-24T05:31:58Z @@ -4090,7 +4090,7 @@ roadmap: github_issue: null item_type: task title: 'OPTMETHODS-1: the interpreter has no Option/Result methods (Some(3).unwrap() fails) and map.get fails' - status: inprogress + status: completed priority: critical assigned_to: null created: 2026-09-24T08:00:00Z @@ -4112,7 +4112,7 @@ roadmap: github_issue: null item_type: task title: 'ARRAYMUT-1: in-place array methods do not mutate (v.sort() leaves v unsorted) and insert/remove/clear/truncate are unknown (interpreter)' - status: inprogress + status: completed priority: critical assigned_to: null created: 2026-09-24T08:00:00Z @@ -4133,7 +4133,7 @@ roadmap: github_issue: null item_type: task title: 'EXTENDKW-1: v.extend([3]) is a parse error "Expected method name, tuple index, or await after ."' - status: inprogress + status: completed priority: critical assigned_to: null created: 2026-09-24T08:00:00Z @@ -4154,7 +4154,7 @@ roadmap: github_issue: null item_type: task title: 'LETVEC-1: let mut v = [1, 2]; v.push(3) transpiles to a fixed-size Rust array and fails rustc E0599' - status: inprogress + status: completed priority: critical assigned_to: null created: 2026-09-24T08:30:00Z @@ -4175,7 +4175,7 @@ roadmap: github_issue: null item_type: task title: 'RESFIELD-1: a field named by a Rust reserved word (o.match, o.type) transpiles to invalid Rust' - status: inprogress + status: completed priority: critical assigned_to: null created: 2026-09-24T08:30:00Z From 9e991bf050ca7b0edccad699e15ee2ee2860ee47 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 08:59:40 +0200 Subject: [PATCH 055/106] RHLGA-1: review fixes F1-F4, F7 (transpiler): scoped LETVEC scan, append, &mut self inference, non-raw keywords - F1 is_grown_as_vec follows scope: a shadowing `let v` ends the scan (its value still counts), a closure/fn with a parameter `v` is skipped; grown_let_as_vec scans siblings with is_grown_in_statements. - F2 `a.append(b)` emits `a.extend_from_slice(&b)`: `b` stays usable. - F3 MUTATING_STD_METHODS gains resize and extend_from_slice; a unit test keeps it a superset of VEC_ONLY_METHODS and the interpreter's in-place table. - F4 `&mut ` counts as a write to self. - F7 transpile_let / transpile_let_with_type use safe_ident, so a let named self/Self/super/crate no longer panics in format_ident!. - Tests: letvec_1 18-22, arraymut_1 16-17 (RED before the fix: wrong vec!, E0382 use-after-move, E0596), unit tests test_rhlga_1_*. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/bindings.rs | 38 ++++--- src/backend/transpiler/call_transpilation.rs | 5 +- src/backend/transpiler/mutation_detection.rs | 109 +++++++++++++++++-- tests/arraymut_1_in_place.rs | 51 +++++++++ tests/letvec_1_growable_locals.rs | 61 +++++++++++ 5 files changed, 239 insertions(+), 25 deletions(-) diff --git a/src/backend/transpiler/bindings.rs b/src/backend/transpiler/bindings.rs index 27a8a30ae..959055955 100644 --- a/src/backend/transpiler/bindings.rs +++ b/src/backend/transpiler/bindings.rs @@ -13,7 +13,7 @@ use super::Transpiler; use crate::frontend::ast::{Expr, ExprKind, Literal, Pattern, Type, TypeKind}; use anyhow::{bail, Result}; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; impl Transpiler { /// Helper: Generate let binding statement with mutability and optional Vec type hint @@ -69,12 +69,8 @@ impl Transpiler { is_mutable: bool, ) -> Result { // Handle Rust reserved keywords by prefixing with r# - let safe_name = if Self::is_rust_reserved_keyword(name) { - format!("r#{name}") - } else { - name.to_string() - }; - let name_ident = format_ident!("{}", safe_name); + // RHLGA-1 F7: `safe_ident` never r#-escapes `self`/`Self`/`super`/`crate`. + let name_ident = Self::safe_ident(name); // Auto-detect mutability let effective_mutability = is_mutable @@ -265,12 +261,8 @@ impl Transpiler { is_mutable: bool, is_const: bool, ) -> Result { - let safe_name = if Self::is_rust_reserved_keyword(name) { - format!("r#{name}") - } else { - name.to_string() - }; - let name_ident = format_ident!("{}", safe_name); + // RHLGA-1 F7: `safe_ident` never r#-escapes `self`/`Self`/`super`/`crate`. + let name_ident = Self::safe_ident(name); // PARSER-073: Generate const/let keyword based on const attribute let is_mutable_var = is_mutable @@ -566,9 +558,7 @@ fn grown_let_as_vec(expr: &Expr, rest: &[Expr]) -> Option { return None; }; let is_statement = matches!(body.kind, ExprKind::Literal(Literal::Unit)); - let grown = rest - .iter() - .any(|e| super::mutation_detection::is_grown_as_vec(name, e)); + let grown = super::mutation_detection::is_grown_in_statements(name, rest); if items.is_empty() || !is_statement || !grown { return None; } @@ -600,6 +590,7 @@ fn is_vec_annotation(ty: &Type) -> bool { mod tests { use super::*; use crate::frontend::ast::Span; + use quote::format_ident; fn make_expr(kind: ExprKind) -> Expr { Expr { @@ -1078,4 +1069,19 @@ mod tests { let expr = ident_expr("items"); assert!(!transpiler.value_creates_vec(&expr)); } + + /// RHLGA-1 review F7: a keyword that cannot be a raw identifier + /// (`self`, `Self`, `super`, `crate`) must not make `let` panic. + #[test] + fn test_rhlga_1_let_named_non_raw_keyword_does_not_panic() { + let transpiler = Transpiler::new(); + for name in ["self", "Self", "super", "crate"] { + let value = int_expr(1); + let body = unit_expr(); + let plain = transpiler.transpile_let(name, &value, &body, false); + assert!(plain.is_ok(), "{name}: {plain:?}"); + let typed = transpiler.transpile_let_with_type(name, None, &value, &body, false, false); + assert!(typed.is_ok(), "{name}: {typed:?}"); + } + } } diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index d86fde493..beb4bf9aa 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -469,8 +469,9 @@ impl Transpiler { | "substring" | "strip" | "lstrip" | "rstrip" | "startswith" | "endswith" | "split" | "replace" => self.transpile_string_methods(obj_tokens, method, arg_tokens), // List methods - // LETVEC-1: Ruchy append(x) adds x's elements (by value), like extend - "append" => Ok(quote! { #obj_tokens.extend(#(#arg_tokens),*) }), + // LETVEC-1: Ruchy append(x) copies x's elements in and leaves x + // usable; extend_from_slice borrows a Vec or an array argument. + "append" => Ok(quote! { #obj_tokens.extend_from_slice(#(&#arg_tokens),*) }), "extend" => Ok(quote! { #obj_tokens.extend(#(#arg_tokens),*) }), // Collection methods "push" | "pop" | "contains" => { diff --git a/src/backend/transpiler/mutation_detection.rs b/src/backend/transpiler/mutation_detection.rs index 7aaebfd91..137985241 100644 --- a/src/backend/transpiler/mutation_detection.rs +++ b/src/backend/transpiler/mutation_detection.rs @@ -4,16 +4,35 @@ //! (reassigned or modified) within expression trees, and whether a method //! body mutates `self` (for `&mut self` receiver inference). -use crate::frontend::ast::{Expr, ExprKind}; +use crate::frontend::ast::{Expr, ExprKind, UnaryOp}; use std::collections::HashSet; /// Std methods that take `&mut self`. A call of one of these on a field of /// `self` (`self.items.push(x)`, `self.buf.push_str(s)`, `self.map.entry(k)`) /// mutates `self`. The list is deliberately closed: a method not listed here /// is treated as non-mutating unless it is a mutating method of the same impl. +/// It is a superset of [`VEC_ONLY_METHODS`] and of the interpreter's in-place +/// array methods (`runtime::eval_array::is_in_place_array_method`). pub const MUTATING_STD_METHODS: &[&str] = &[ - "push", "pop", "insert", "remove", "clear", "extend", "append", "truncate", "sort", "sort_by", - "reverse", "dedup", "retain", "drain", "push_str", "swap", "entry", + "push", + "pop", + "insert", + "remove", + "clear", + "extend", + "extend_from_slice", + "append", + "truncate", + "resize", + "sort", + "sort_by", + "reverse", + "dedup", + "retain", + "drain", + "push_str", + "swap", + "entry", ]; /// LETVEC-1: methods that exist on `Vec` but not on a fixed-size array. A @@ -36,9 +55,58 @@ pub const VEC_ONLY_METHODS: &[&str] = &[ ]; /// LETVEC-1: true when the variable `name` is the receiver of a -/// [`VEC_ONLY_METHODS`] call anywhere in `expr`. +/// [`VEC_ONLY_METHODS`] call in `expr` that refers to this binding of `name`. +/// +/// The scan follows scope: a `let name = …` ends it (its value still refers +/// to the outer binding, its body and the block statements after it do not), +/// and a closure or nested function with a parameter `name` is skipped. pub fn is_grown_as_vec(name: &str, expr: &Expr) -> bool { - any_expr(expr, &|e| is_vec_only_call_on(name, e)) + if is_vec_only_call_on(name, expr) { + return true; + } + match &expr.kind { + ExprKind::Block(exprs) => is_grown_in_statements(name, exprs), + ExprKind::Let { + name: bound, + value, + else_block, + .. + } if bound == name => { + is_grown_as_vec(name, value) + || else_block + .as_deref() + .is_some_and(|e| is_grown_as_vec(name, e)) + } + _ if binds_param(name, expr) => false, + _ => sub_expressions(expr) + .into_iter() + .any(|e| is_grown_as_vec(name, e)), + } +} + +/// A closure or nested function with a parameter called `name`. +fn binds_param(name: &str, expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Lambda { params, .. } | ExprKind::Function { params, .. } => { + params.iter().any(|p| p.name() == name) + } + ExprKind::AsyncLambda { params, .. } => params.iter().any(|p| p == name), + _ => false, + } +} + +/// LETVEC-1: [`is_grown_as_vec`] over a statement sequence: the statements +/// are scanned in order until one of them re-binds `name` with a `let`. +pub fn is_grown_in_statements(name: &str, statements: &[Expr]) -> bool { + for statement in statements { + if is_grown_as_vec(name, statement) { + return true; + } + if matches!(&statement.kind, ExprKind::Let { name: bound, .. } if bound == name) { + return false; + } + } + false } /// `name.(..)` with `m` a [`VEC_ONLY_METHODS`] method. @@ -120,9 +188,14 @@ fn is_self_mutating_call(expr: &Expr, mutating_methods: &HashSet) -> boo } } -/// Assignment, compound assignment or `++`/`--` whose target place is rooted at `name`. +/// Assignment, compound assignment or `++`/`--` whose target place is rooted +/// at `name`, or a `&mut ` borrow (passed to a call). fn writes_place_rooted_at(name: &str, expr: &Expr) -> bool { match &expr.kind { + ExprKind::Unary { + op: UnaryOp::MutableReference, + operand, + } => place_root(operand) == Some(name), ExprKind::Assign { target, .. } | ExprKind::CompoundAssign { target, .. } | ExprKind::PreIncrement { target } @@ -413,7 +486,7 @@ fn with_optional<'a>(required: Vec<&'a Box>, optional: Option<&'a Expr>) - #[cfg(test)] mod tests { use super::*; - use crate::frontend::ast::{BinaryOp, Literal, MatchArm, Pattern, Span, UnaryOp}; + use crate::frontend::ast::{BinaryOp, Literal, MatchArm, Pattern, Span}; // ==================== Test Helpers ==================== @@ -1673,4 +1746,26 @@ mod tests { // `return (x = 1)` assigns x before returning (G2BFA2). assert!(is_variable_mutated("x", &ret)); } + + /// RHLGA-1 review F3: every Vec-only method and every in-place array + /// method the interpreter knows is a mutating std method, so a call of + /// one on `self.` infers `&mut self`. + #[test] + fn test_rhlga_1_mutating_std_methods_cover_in_place_tables() { + let in_place = [ + "push", "pop", "insert", "remove", "clear", "extend", "append", + ] + .into_iter() + .chain(["truncate", "sort", "reverse", "dedup", "resize"]) + .chain(["extend_from_slice", "retain", "drain", "sort_by"]); + for method in in_place.chain(VEC_ONLY_METHODS.iter().copied()) { + let is_runtime_in_place = + (0..3).any(|n| crate::runtime::eval_array::is_in_place_array_method(method, n)); + let known = VEC_ONLY_METHODS.contains(&method) || is_runtime_in_place; + assert!( + !known || MUTATING_STD_METHODS.contains(&method), + "{method} is in-place but not in MUTATING_STD_METHODS" + ); + } + } } diff --git a/tests/arraymut_1_in_place.rs b/tests/arraymut_1_in_place.rs index aca391a92..0a65cb092 100644 --- a/tests/arraymut_1_in_place.rs +++ b/tests/arraymut_1_in_place.rs @@ -258,3 +258,54 @@ fun main() {{ " )); } + +// ---------- RHLGA-1 review F3/F4: `&mut self` inference ---------- + +/// Transpile, compile with rustc, run; the rustc output. +fn through_rustc(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), src); + rustc_run(dir.path(), &rust) +} + +#[test] +fn test_arraymut_1_16_resize_and_extend_from_slice_infer_mut_self() { + let src = "struct Bag { items: Vec } +impl Bag { + fun grow(self) { + self.items.resize(3, 0) + } + fun more(self) { + self.items.extend_from_slice(&[9]) + } +} +fun main() { + let mut b = Bag { items: vec![1] } + b.grow() + b.more() + println(b.items) +} +"; + assert_eq!(through_rustc(src), "[1, 0, 0, 9]\n"); +} + +#[test] +fn test_arraymut_1_17_mut_borrow_of_self_field_infers_mut_self() { + let src = "struct Bag { items: Vec } +fun add_one(xs: &mut Vec) -> usize { + xs.push(1) + xs.len() +} +impl Bag { + fun fill(self) -> usize { + add_one(&mut self.items) + } +} +fun main() { + let mut b = Bag { items: vec![7] } + println(b.fill()) + println(b.items) +} +"; + assert_eq!(through_rustc(src), "2\n[7, 1]\n"); +} diff --git a/tests/letvec_1_growable_locals.rs b/tests/letvec_1_growable_locals.rs index 7bc3016a6..4548e2ce3 100644 --- a/tests/letvec_1_growable_locals.rs +++ b/tests/letvec_1_growable_locals.rs @@ -236,3 +236,64 @@ fn test_letvec_1_17_growth_inside_nested_block() { assert_eq!(expected, "4\n"); assert_eq!(compile_and_run(dir.path(), src), expected); } + +// ---------- RHLGA-1 review F1: the scan follows scope ---------- + +#[test] +fn test_letvec_1_18_shadowing_let_ends_the_scan() { + let src = "fun main() {\n let v = [1, 2]\n println(v.len())\n let mut v = [3]\n v.push(4)\n println(v.len())\n}\n"; + let dir = tempfile::tempdir().expect("tempdir"); + let rust = squash(&transpile(dir.path(), src)); + assert!( + rust.contains("letv=[1,2];"), + "outer v stays an array:\n{rust}" + ); + assert!( + rust.contains("letmutv=vec![3];"), + "the shadowing v becomes a vec:\n{rust}" + ); + assert_eq!(compile_and_run(dir.path(), src), "2\n2\n"); +} + +#[test] +fn test_letvec_1_19_nested_block_rebinding_does_not_count() { + let src = "fun main() {\n let v = [1, 2]\n if true {\n let mut v = [5]\n v.push(6)\n println(v.len())\n }\n println(v.len())\n}\n"; + let dir = tempfile::tempdir().expect("tempdir"); + let rust = squash(&transpile(dir.path(), src)); + assert!( + rust.contains("letv=[1,2];"), + "outer v stays an array:\n{rust}" + ); + assert_eq!(compile_and_run(dir.path(), src), "2\n2\n"); +} + +#[test] +fn test_letvec_1_20_closure_param_rebinding_does_not_count() { + let src = + "fun main() {\n let v = [1, 2]\n let f = |v| v.push(3)\n println(v.len())\n}\n"; + let rust = squash(&transpile_str(src)); + assert!( + rust.contains("letv=[1,2];"), + "outer v stays an array:\n{rust}" + ); +} + +#[test] +fn test_letvec_1_21_shadowing_value_still_sees_the_outer_binding() { + let src = "fun main() {\n let mut v = [1]\n let v = { v.push(2); v }\n println(v.len())\n}\n"; + let rust = squash(&transpile_str(src)); + assert!(rust.contains("letmutv=vec![1];"), "outer v grows:\n{rust}"); +} + +// ---------- RHLGA-1 review F2: append copies the argument in ---------- + +#[test] +fn test_letvec_1_22_append_leaves_argument_usable_through_rustc() { + let src = "fun main() {\n let mut a = [1, 2]\n let mut b = [3]\n b.push(4)\n a.append(b)\n println(a)\n println(b)\n let c = [5, 6]\n a.append(c)\n println(a.len() + c.len())\n}\n"; + let dir = tempfile::tempdir().expect("tempdir"); + let expected = interpret(dir.path(), src); + assert_eq!(expected, "[1, 2, 3, 4]\n[3, 4]\n8\n"); + let rust = squash(&transpile(dir.path(), src)); + assert!(rust.contains("a.extend_from_slice(&b)"), "rust:\n{rust}"); + assert_eq!(compile_and_run(dir.path(), src), expected); +} From 822059cdb2ab2b079195451bedd2c1f250cdc66d Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:00:19 +0200 Subject: [PATCH 056/106] RHLGA-1: review fixes F5, F6 (interpreter): user enum Some/None, nested field writes - F5 the Option bridge only claims EnumVariants whose enum name is Option, Result or empty; a user `enum Maybe { Some(i32), None }` keeps its own methods. dispatch_native_method_call now runs a user impl method on an enum value (EnumName::method with the variant bound to self); before, every enum-value method fell to the generic table ("not found"). - F6 try_field_array_mutation accepts any field chain rooted at a variable; eval_field_assign writes a nested field back through each parent (`self.inner.items.push(x)`, `o.a.b.sort()`). - Tests: optmethods_1_14, fieldpop_1_11-13 (RED before the fix: Some(3)/true, lost pushes and sorts). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 584 ++++++++++---------- src/runtime/eval_option_methods.rs | 14 +- src/runtime/interpreter_control_flow.rs | 87 +-- src/runtime/interpreter_methods_dispatch.rs | 41 +- tests/fieldpop_1_field_methods.rs | 51 ++ tests/optmethods_1_option_methods.rs | 24 + 6 files changed, 466 insertions(+), 335 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index b1d07b041..56657f43c 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T06:15:17.732523048Z", + "created_at": "2026-09-24T07:14:59.316558599Z", "git_context": null, "files": { "./build.rs": { @@ -3040,48 +3040,48 @@ }, "./src/backend/transpiler/bindings.rs": { "content_hash": [ - 197, - 185, - 36, - 78, - 237, - 200, - 139, - 29, - 206, - 234, - 3, - 169, - 117, - 133, - 134, - 238, - 231, - 202, - 135, - 67, - 149, - 66, - 105, - 105, - 141, - 10, - 247, + 118, + 130, + 37, + 102, + 47, + 120, 163, - 24, + 26, + 245, + 78, + 230, + 60, 64, + 24, + 108, + 151, + 205, + 248, + 57, + 8, + 174, + 127, + 195, + 159, 96, - 162 + 176, + 21, + 251, + 4, + 128, + 250, + 118 ], "score": { - "structural_complexity": 5.0999994, + "structural_complexity": 3.5, "semantic_complexity": 20.0, - "duplication_ratio": 13.807909, + "duplication_ratio": 14.004329, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 78.907906, + "total": 77.50433, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3093,31 +3093,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 97 duplicate code patterns" + "issue": "Found 96 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.1920905, + "amount": 5.9956713, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 31.0%" + "issue": "Code duplication: 30.0%" }, { "source_metric": "StructuralComplexity", - "amount": 9.900001, + "amount": 10.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 48" + "issue": "High cognitive complexity: 50" }, { "source_metric": "StructuralComplexity", - "amount": 10.0, + "amount": 11.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 50" + "issue": "High cyclomatic complexity: 53" } ], "critical_defects_count": 0, @@ -3238,38 +3238,38 @@ }, "./src/backend/transpiler/block_transpiler.rs": { "content_hash": [ - 24, - 53, - 7, - 16, - 134, - 199, - 234, - 124, - 168, - 127, + 10, + 237, + 81, + 18, + 38, + 128, + 2, + 56, + 74, + 183, + 204, + 110, + 13, + 3, + 252, + 46, + 193, + 226, 238, - 234, - 95, - 42, - 7, - 97, - 139, - 64, - 218, - 36, - 165, - 163, - 172, - 197, - 153, - 180, + 215, + 61, + 76, + 196, + 181, + 61, + 106, 10, - 67, - 156, - 55, - 206, - 157 + 53, + 171, + 70, + 11, + 187 ], "score": { "structural_complexity": 23.8, @@ -3325,48 +3325,48 @@ }, "./src/backend/transpiler/body_generation.rs": { "content_hash": [ - 207, - 74, - 61, + 47, + 146, + 216, + 144, + 100, + 198, + 186, + 146, + 38, + 246, + 106, + 253, + 24, + 80, + 96, + 57, + 128, + 135, + 143, + 203, + 214, + 69, + 62, + 89, + 77, + 129, + 155, 176, - 6, - 196, - 179, - 45, - 111, - 251, - 126, - 254, - 78, - 199, - 94, - 75, - 137, - 142, - 245, - 118, - 109, - 162, - 160, - 109, - 40, - 56, - 152, - 160, - 109, - 16, - 67, - 239 + 119, + 149, + 82, + 185 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.356276, + "duplication_ratio": 16.437246, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.14207, + "total": 92.21568, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -3378,15 +3378,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 27 duplicate code patterns" + "issue": "Found 26 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.6437247, + "amount": 3.562753, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 18.2%" + "issue": "Code duplication: 17.8%" } ], "critical_defects_count": 0, @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 96, - 232, - 11, - 3, - 248, - 235, - 105, - 166, - 149, - 163, - 45, - 151, - 174, - 153, - 132, 127, - 185, - 51, - 76, - 15, - 61, - 131, - 191, - 97, - 40, - 0, - 78, + 172, + 155, + 46, + 2, + 208, 35, - 83, - 199, - 222, - 86 + 32, + 219, + 205, + 180, + 195, + 142, + 126, + 241, + 12, + 239, + 201, + 16, + 215, + 181, + 172, + 154, + 224, + 91, + 144, + 90, + 3, + 248, + 101, + 72, + 114 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.912939, + "duplication_ratio": 15.888754, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.91294, + "total": 75.888756, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3639,15 +3639,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 68 duplicate code patterns" + "issue": "Found 69 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.0870614, + "amount": 4.1112456, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.4%" + "issue": "Code duplication: 20.6%" }, { "source_metric": "StructuralComplexity", @@ -7220,49 +7220,49 @@ }, "./src/backend/transpiler/mutation_detection.rs": { "content_hash": [ - 9, - 166, - 164, - 187, - 134, - 75, - 93, - 42, - 245, - 213, - 82, - 97, - 234, - 158, - 175, + 14, + 27, + 63, + 179, + 154, + 76, + 90, + 59, + 255, + 32, 226, - 42, - 62, - 21, 176, - 63, - 0, - 119, - 186, - 108, - 198, - 88, - 181, - 30, - 222, - 27, - 88 + 43, + 142, + 17, + 251, + 123, + 32, + 163, + 140, + 104, + 226, + 58, + 139, + 209, + 237, + 246, + 52, + 253, + 92, + 28, + 132 ], "score": { - "structural_complexity": 17.2, + "structural_complexity": 8.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.07351, + "duplication_ratio": 17.080868, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 94.27351, - "grade": "A", + "total": 85.08087, + "grade": "A-", "confidence": 1.0, "language": "Rust", "file_path": "./src/backend/transpiler/mutation_detection.rs", @@ -7273,11 +7273,11 @@ "applied_to": [ "Duplication" ], - "issue": "Found 114 duplicate code patterns" + "issue": "Found 131 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.9264908, + "amount": 2.9191322, "applied_to": [ "Duplication" ], @@ -7285,19 +7285,19 @@ }, { "source_metric": "StructuralComplexity", - "amount": 0.3, + "amount": 3.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 16" + "issue": "High cognitive complexity: 25" }, { "source_metric": "StructuralComplexity", - "amount": 7.5, + "amount": 14.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 45" + "issue": "High cyclomatic complexity: 58" } ], "critical_defects_count": 0, @@ -17056,48 +17056,48 @@ }, "./src/frontend/parser/expressions_helpers/structs.rs": { "content_hash": [ - 234, - 161, - 71, - 34, - 32, - 58, - 155, - 178, - 222, - 1, + 4, + 46, + 59, + 172, + 57, + 93, + 38, + 85, + 68, + 207, + 113, + 187, + 227, + 219, + 133, + 96, + 136, + 47, + 130, 39, - 11, - 151, - 192, - 180, - 15, - 247, - 186, - 44, + 168, 93, - 191, - 95, - 31, - 1, - 232, - 65, - 145, - 14, - 126, - 13, - 29, - 16 + 131, + 212, + 122, + 2, + 131, + 151, + 25, + 72, + 24, + 162 ], "score": { - "structural_complexity": 3.0, + "structural_complexity": 2.5, "semantic_complexity": 20.0, - "duplication_ratio": 16.533638, + "duplication_ratio": 16.572077, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 79.53364, + "total": 79.072075, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -17109,15 +17109,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 47 duplicate code patterns" + "issue": "Found 46 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.4663627, + "amount": 3.4279227, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 17.3%" + "issue": "Code duplication: 17.1%" }, { "source_metric": "StructuralComplexity", @@ -17129,11 +17129,11 @@ }, { "source_metric": "StructuralComplexity", - "amount": 12.0, + "amount": 12.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 54" + "issue": "High cyclomatic complexity: 55" } ], "critical_defects_count": 0, @@ -18377,48 +18377,48 @@ }, "./src/frontend/parser/types.rs": { "content_hash": [ - 58, - 253, - 21, - 253, - 43, - 190, - 239, - 222, - 218, - 179, - 238, - 149, - 53, - 196, - 16, - 57, - 42, - 104, - 194, - 216, + 241, + 113, + 1, + 31, + 221, + 129, + 160, 180, + 85, + 228, + 190, 103, - 96, - 129, - 195, - 112, - 65, - 159, - 70, - 159, - 140, - 180 + 18, + 60, + 69, + 58, + 106, + 143, + 89, + 124, + 46, + 100, + 165, + 120, + 32, + 167, + 97, + 108, + 193, + 218, + 218, + 154 ], "score": { "structural_complexity": 24.7, "semantic_complexity": 20.0, - "duplication_ratio": 16.02649, + "duplication_ratio": 16.129032, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 91.569534, + "total": 91.66276, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -18434,11 +18434,11 @@ }, { "source_metric": "Duplication", - "amount": 3.97351, + "amount": 3.8709676, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 19.9%" + "issue": "Code duplication: 19.4%" }, { "source_metric": "StructuralComplexity", @@ -33703,48 +33703,48 @@ }, "./src/runtime/eval_array.rs": { "content_hash": [ - 236, - 241, - 133, - 38, - 236, - 187, - 230, - 248, - 101, - 77, - 124, - 123, - 166, - 159, - 223, - 196, - 184, + 222, + 86, + 25, + 71, + 149, + 90, + 87, + 93, + 81, + 160, + 15, + 137, + 200, + 233, + 179, + 80, + 150, + 100, 208, - 168, - 55, - 152, - 52, - 245, - 131, - 176, - 44, - 177, - 180, - 155, - 247, - 216, - 96 + 225, + 241, + 99, + 203, + 128, + 164, + 29, + 153, + 45, + 161, + 51, + 65, + 219 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.124031, + "duplication_ratio": 16.14272, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.12403, + "total": 76.142715, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33760,11 +33760,11 @@ }, { "source_metric": "Duplication", - "amount": 3.875969, + "amount": 3.8572807, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 19.4%" + "issue": "Code duplication: 19.3%" }, { "source_metric": "StructuralComplexity", @@ -33780,7 +33780,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 167" + "issue": "High cyclomatic complexity: 168" } ], "critical_defects_count": 0, @@ -45046,11 +45046,11 @@ }, "summary": { "total_files": 530, - "avg_score": 90.310196, + "avg_score": 90.289635, "grade_distribution": { "A+": 195, - "A": 176, - "A-": 33, + "A": 175, + "A-": 34, "B+": 28, "B": 69, "B-": 26, diff --git a/src/runtime/eval_option_methods.rs b/src/runtime/eval_option_methods.rs index e4150c531..6b92b5f25 100644 --- a/src/runtime/eval_option_methods.rs +++ b/src/runtime/eval_option_methods.rs @@ -53,17 +53,25 @@ pub fn is_unknown_method_error(err: &InterpreterError, method: &str) -> bool { names_method && (msg.contains("Unknown") || msg.contains("not found")) } -/// The variant name and payload of a `Some`/`None`/`Ok`/`Err` value. +/// The variant name and payload of a builtin `Some`/`None`/`Ok`/`Err` value. +/// The interpreter builds those with enum name `Option`/`Result`; an empty +/// name is accepted too. A user enum's own `Some`/`None`/`Ok`/`Err` variant +/// (`enum Maybe { Some(i32), None }`) is not one: its methods dispatch normally. /// /// # Complexity -/// Cyclomatic complexity: 3 +/// Cyclomatic complexity: 4 fn variant_payload(receiver: &Value) -> Option<(&str, Option<&Value>)> { let Value::EnumVariant { - variant_name, data, .. + enum_name, + variant_name, + data, } = receiver else { return None; }; + if !matches!(enum_name.as_str(), "" | "Option" | "Result") { + return None; + } let payload = data.as_ref().and_then(|d| d.first()); match variant_name.as_str() { "Some" | "None" | "Ok" | "Err" => Some((variant_name.as_str(), payload)), diff --git a/src/runtime/interpreter_control_flow.rs b/src/runtime/interpreter_control_flow.rs index ebea81c20..eaa0a3470 100644 --- a/src/runtime/interpreter_control_flow.rs +++ b/src/runtime/interpreter_control_flow.rs @@ -487,55 +487,68 @@ impl Interpreter { /// Evaluate field assignment: `obj.field = value`. /// /// Handles Object, ObjectMut, Class, and Struct field updates. + /// `object` is a variable or a field chain rooted at one (`o.inner.items = …`): a value-typed parent is + /// rebuilt with the new field and written back to its own parent in turn. pub(crate) fn eval_field_assign( &mut self, object: &Expr, field: &str, val: Value, ) -> Result { - let ExprKind::Identifier(obj_name) = &object.kind else { - return Err(InterpreterError::RuntimeError( - "Complex field access not supported".to_string(), - )); - }; - let obj = self.lookup_variable(obj_name)?; - - match obj { - Value::Object(ref map) => { - let mut new_map = (**map).clone(); - new_map.insert(field.to_string(), val.clone()); - self.set_variable(obj_name, Value::Object(Arc::new(new_map))); - Ok(val) - } - Value::ObjectMut(ref cell) => { - cell.lock() - .expect("Mutex poisoned: object lock is corrupted") - .insert(field.to_string(), val.clone()); - Ok(val) + /// `obj` with `field` set to `val`: a copy for value types, the same + /// shared value (updated in place) for `ObjectMut` and `Class`. + fn with_field(obj: Value, field: &str, val: Value) -> Result { + match obj { + Value::Object(map) => { + let mut new_map = (*map).clone(); + new_map.insert(field.to_string(), val); + Ok(Value::Object(Arc::new(new_map))) + } + Value::ObjectMut(ref cell) => { + cell.lock() + .expect("Mutex poisoned: object lock is corrupted") + .insert(field.to_string(), val); + Ok(obj) + } + Value::Class { ref fields, .. } => { + fields + .write() + .expect("RwLock poisoned: class fields lock is corrupted") + .insert(field.to_string(), val); + Ok(obj) + } + Value::Struct { name, fields } => { + let mut new_fields = (*fields).clone(); + new_fields.insert(field.to_string(), val); + Ok(Value::Struct { + name, + fields: Arc::new(new_fields), + }) + } + _ => Err(InterpreterError::RuntimeError(format!( + "Cannot access field '{field}' on non-object" + ))), } - Value::Class { ref fields, .. } => { - fields - .write() - .expect("RwLock poisoned: class fields lock is corrupted") - .insert(field.to_string(), val.clone()); + } + match &object.kind { + ExprKind::Identifier(obj_name) => { + let obj = self.lookup_variable(obj_name)?; + let updated = with_field(obj, field, val.clone())?; + self.set_variable(obj_name, updated); Ok(val) } - Value::Struct { - ref name, - ref fields, + ExprKind::FieldAccess { + object: parent, + field: parent_field, } => { - let mut new_fields = (**fields).clone(); - new_fields.insert(field.to_string(), val.clone()); - let new_struct = Value::Struct { - name: name.clone(), - fields: Arc::new(new_fields), - }; - self.set_variable(obj_name, new_struct); + let obj = self.eval_expr(object)?; + let updated = with_field(obj, field, val.clone())?; + self.eval_field_assign(parent, parent_field, updated)?; Ok(val) } - _ => Err(InterpreterError::RuntimeError(format!( - "Cannot access field '{field}' on non-object" - ))), + _ => Err(InterpreterError::RuntimeError( + "Complex field access not supported".to_string(), + )), } } diff --git a/src/runtime/interpreter_methods_dispatch.rs b/src/runtime/interpreter_methods_dispatch.rs index b65661ab3..8611f7397 100644 --- a/src/runtime/interpreter_methods_dispatch.rs +++ b/src/runtime/interpreter_methods_dispatch.rs @@ -47,11 +47,22 @@ impl ObjectKind { } } +/// RHLGA-1 F6: `x`, `x.a`, `x.a.b`, …: a field chain rooted at a variable, +/// which `eval_field_assign` can write back through. +fn is_field_chain_rooted(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Identifier(_) => true, + ExprKind::FieldAccess { object, .. } => is_field_chain_rooted(object), + _ => false, + } +} + impl Interpreter { /// FIELDPOP-1 / ARRAYMUT-1: an in-place array method on a field /// (`self.items.pop()`, `s.items.sort()`, `o.items.insert(0, x)`) behaves /// like the same call on a local array: the call returns the method's own - /// result and the changed array is written back to the field. + /// result and the changed array is written back to the field. The field + /// may be nested (`self.inner.items.push(x)`, `s.a.b.sort()`). /// /// Returns `Ok(None)` when the call is not of that shape, so the caller /// falls through to ordinary method dispatch. @@ -64,8 +75,7 @@ impl Interpreter { let ExprKind::FieldAccess { object, field } = &receiver.kind else { return Ok(None); }; - let rooted = matches!(object.kind, ExprKind::Identifier(_)); - if !rooted || !is_in_place_array_method(method, args.len()) { + if !is_field_chain_rooted(object) || !is_in_place_array_method(method, args.len()) { return Ok(None); } let Value::Array(arr) = self.eval_expr(receiver)? else { @@ -101,6 +111,28 @@ impl Interpreter { Ok(Some(result)) } + /// RHLGA-1 F5: a method of a user `impl` on an enum value + /// (`Maybe::Some(3).unwrap()`) runs with the variant bound to `self`. + /// `None` when the enum has no such method. + fn eval_enum_instance_method( + &mut self, + receiver: &Value, + enum_name: &str, + method: &str, + arg_values: &[Value], + ) -> Option> { + let closure = self + .lookup_variable(&format!("{enum_name}::{method}")) + .ok()?; + if !matches!(closure, Value::Closure { .. }) { + return None; + } + let args: Vec = std::iter::once(receiver.clone()) + .chain(arg_values.iter().cloned()) + .collect(); + Some(self.call_function(closure, &args)) + } + /// Evaluate `args`, then apply the in-place `method` to a copy of `arr`. /// Returns the changed elements and the method's result. fn apply_in_place_call( @@ -417,6 +449,9 @@ impl Interpreter { arg_values, ) } + Value::EnumVariant { enum_name, .. } => self + .eval_enum_instance_method(receiver, enum_name, base_method, arg_values) + .unwrap_or_else(|| self.eval_generic_method(receiver, base_method, args_empty)), #[cfg(not(target_arch = "wasm32"))] Value::HtmlDocument(doc) => { self.eval_html_document_method(doc, base_method, arg_values) diff --git a/tests/fieldpop_1_field_methods.rs b/tests/fieldpop_1_field_methods.rs index eea6e7686..26a34b952 100644 --- a/tests/fieldpop_1_field_methods.rs +++ b/tests/fieldpop_1_field_methods.rs @@ -292,3 +292,54 @@ fun main() { let squashed: String = rust.chars().filter(|c| !c.is_whitespace()).collect(); assert!(squashed.contains("fixed:[7,8]"), "rust:\n{rust}"); } + +// ---------- RHLGA-1 review F6: in-place methods on nested fields ---------- + +const NESTED: &str = "struct Inner { items: Vec } +struct Outer { inner: Inner } +"; + +#[test] +fn test_fieldpop_1_11_nested_self_field_push_persists() { + let src = format!( + "{NESTED}impl Outer {{ + fun add(&mut self, x: i32) {{ + self.inner.items.push(x) + }} +}} +fun main() {{ + let mut o = Outer {{ inner: Inner {{ items: vec![1] }} }} + o.add(5) + o.add(3) + println(o.inner.items) +}} +" + ); + assert_eq!(run_file(&src), "[1, 5, 3]\n"); +} + +#[test] +fn test_fieldpop_1_12_nested_local_field_sort_and_pop_persist() { + let src = format!( + "{NESTED}fun main() {{ + let mut o = Outer {{ inner: Inner {{ items: vec![3, 1, 2] }} }} + o.inner.items.sort() + println(o.inner.items) + println(o.inner.items.pop()) + println(o.inner.items) +}} +" + ); + assert_eq!(run_file(&src), "[1, 2, 3]\n3\n[1, 2]\n"); +} + +#[test] +fn test_fieldpop_1_13_object_literal_nested_field_push_persists() { + let src = "fun main() { + let mut o = { a: { b: [1] } } + o.a.b.push(2) + println(o.a.b) +} +"; + assert_eq!(run_file(src), "[1, 2]\n"); +} diff --git a/tests/optmethods_1_option_methods.rs b/tests/optmethods_1_option_methods.rs index bdf0cd60e..4e7c68771 100644 --- a/tests/optmethods_1_option_methods.rs +++ b/tests/optmethods_1_option_methods.rs @@ -336,3 +336,27 @@ fn test_optmethods_1_13_differential_pop_unwrap_through_rustc() { ); assert_eq!(differential(&src), "3\n2\n1\n0\ntrue\n"); } + +// ---------- RHLGA-1 review F5: a user enum's `Some`/`None` is not Option ---------- + +#[test] +fn test_optmethods_1_14_user_enum_variant_named_some_keeps_its_methods() { + let src = "enum Maybe { Some(i32), None } +impl Maybe { + fun unwrap(self) -> i32 { + 42 + } + fun is_some(self) -> bool { + false + } +} +fun main() { + let m = Maybe::Some(3) + println(m.unwrap()) + println(m.is_some()) + let n = Maybe::None + println(n.unwrap()) +} +"; + assert_eq!(run_file(src), "42\nfalse\n42\n"); +} From 4022a6ea6191d87f4cab69e7e386a67bcb52e913 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:00:29 +0200 Subject: [PATCH 057/106] RHLGA-1: review fix F8 (compile test writes a.out to a temp dir); CHANGELOG - F8 test_transpiler_scope_compile_command runs `ruchy compile` with current_dir set to a temp dir (before, a.out landed in the repo root), and asserts a.out is there. - CHANGELOG G2 part 2: scoped LETVEC scan; append -> extend_from_slice marked (behaviour); resize/extend_from_slice/&mut self.field receiver inference; user enum Some/None and enum impl methods; nested field writes; non-raw keyword lets. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 234 +++++++++++++-------------- CHANGELOG.md | 24 ++- tests/transpiler_scope_global_mut.rs | 16 +- 3 files changed, 145 insertions(+), 129 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 56657f43c..dd60c8de4 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T07:14:59.316558599Z", + "created_at": "2026-09-24T07:15:02.015379570Z", "git_context": null, "files": { "./build.rs": { @@ -35849,49 +35849,49 @@ }, "./src/runtime/eval_option_methods.rs": { "content_hash": [ - 93, - 22, - 85, - 26, + 138, + 7, + 107, + 168, + 198, + 45, + 95, + 6, + 188, + 102, + 45, + 84, + 216, + 230, + 233, + 190, + 217, 28, - 114, - 202, - 203, - 247, + 9, + 126, 239, - 42, - 220, + 34, + 94, 195, - 231, - 148, - 254, - 245, - 49, - 142, - 23, - 226, - 169, - 38, - 162, - 71, - 37, - 47, - 205, - 200, - 77, - 25, - 186 + 179, + 123, + 208, + 120, + 167, + 107, + 131, + 157 ], "score": { - "structural_complexity": 8.4, + "structural_complexity": 7.6, "semantic_complexity": 20.0, - "duplication_ratio": 17.311476, + "duplication_ratio": 17.290323, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 85.71147, - "grade": "A-", + "total": 84.89032, + "grade": "B+", "confidence": 1.0, "language": "Rust", "file_path": "./src/runtime/eval_option_methods.rs", @@ -35902,31 +35902,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 25 duplicate code patterns" + "issue": "Found 26 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.6885247, + "amount": 2.7096775, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.4%" + "issue": "Code duplication: 13.5%" }, { "source_metric": "StructuralComplexity", - "amount": 2.1000001, + "amount": 2.4, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 22" + "issue": "High cognitive complexity: 23" }, { "source_metric": "StructuralComplexity", - "amount": 14.5, + "amount": 15.0, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 59" + "issue": "High cyclomatic complexity: 60" } ], "critical_defects_count": 0, @@ -36973,48 +36973,48 @@ }, "./src/runtime/interpreter_control_flow.rs": { "content_hash": [ - 215, - 40, - 223, - 133, - 42, - 246, - 3, - 200, - 109, - 228, - 162, - 246, - 208, - 209, 45, - 213, - 92, - 73, + 184, + 182, + 50, + 164, + 125, + 45, + 109, + 175, + 163, + 101, + 221, + 42, + 224, + 76, + 12, + 141, + 105, + 59, + 43, + 196, + 183, + 7, + 85, 193, - 3, - 103, - 233, - 152, - 142, - 234, - 202, - 108, - 230, - 65, - 73, - 5, - 132 + 161, + 76, + 186, + 87, + 216, + 148, + 210 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 13.962264, + "duplication_ratio": 13.990285, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 73.962265, + "total": 73.99028, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -37026,15 +37026,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 184 duplicate code patterns" + "issue": "Found 187 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.037736, + "amount": 6.009715, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 30.2%" + "issue": "Code duplication: 30.0%" }, { "source_metric": "StructuralComplexity", @@ -37042,7 +37042,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 99" + "issue": "High cognitive complexity: 100" }, { "source_metric": "StructuralComplexity", @@ -37050,7 +37050,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 111" + "issue": "High cyclomatic complexity: 114" } ], "critical_defects_count": 0, @@ -37542,48 +37542,48 @@ }, "./src/runtime/interpreter_methods_dispatch.rs": { "content_hash": [ - 173, - 241, - 155, - 127, - 201, - 26, + 44, + 67, 201, - 17, - 213, - 107, - 140, - 113, - 132, - 134, - 133, - 253, - 113, - 193, - 237, + 46, + 166, + 220, + 67, 243, - 185, - 113, - 64, - 209, - 234, + 254, + 157, + 91, + 127, + 212, + 197, + 23, + 206, + 31, 131, - 50, - 13, - 63, - 71, - 15, - 102 + 181, + 163, + 68, + 158, + 121, + 103, + 130, + 26, + 77, + 181, + 77, + 243, + 5, + 217 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.254144, + "duplication_ratio": 14.305085, "coupling_score": 11.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 70.25414, + "total": 70.305084, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -37595,15 +37595,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 165 duplicate code patterns" + "issue": "Found 166 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.7458563, + "amount": 5.694915, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 28.7%" + "issue": "Code duplication: 28.5%" }, { "source_metric": "StructuralComplexity", @@ -37611,7 +37611,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 59" + "issue": "High cognitive complexity: 61" }, { "source_metric": "StructuralComplexity", @@ -37619,7 +37619,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 73" + "issue": "High cyclomatic complexity: 78" }, { "source_metric": "Coupling", @@ -45046,12 +45046,12 @@ }, "summary": { "total_files": 530, - "avg_score": 90.289635, + "avg_score": 90.288246, "grade_distribution": { "A+": 195, "A": 175, - "A-": 34, - "B+": 28, + "A-": 33, + "B+": 29, "B": 69, "B-": 26, "C": 1, diff --git a/CHANGELOG.md b/CHANGELOG.md index 9945a0c3c..325392646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ Behaviour changes a program can observe are marked **(behaviour)**. a borrowing getter. This reverses the 2026-01 change that always borrowed. Mutation is detected through nested fields, indexes, `++`/`--`, mutating std methods on a field (`push`, `insert`, …) and calls to other mutating - methods of the same impl. A trait default method taking an owned `self` gets + methods of the same impl. The mutating std methods include `resize` and + `extend_from_slice`, and passing `&mut self.field` to a call mutates `self`. + A trait default method taking an owned `self` gets `where Self: Sized`, so it compiles. - A function or method named with a Rust reserved word (`do`, `box`, `typeof`, …) is emitted as a raw identifier (`r#do`) at definition and call. @@ -54,7 +56,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. `v.pop().unwrap()` yields the element. A plain value's own methods win. `unwrap`/`expect` on an absent value is an error naming it, and so is `unwrap_or_default`, whose default type is unknown at runtime. A plain - object has `get(key)`, returning the value or nil. + object has `get(key)`, returning the value or nil. Only the builtin + `Option`/`Result` variants take these methods first: a user enum's own + `Some`/`None` variant keeps its `impl` methods, which the interpreter now + dispatches on enum values. - **(behaviour)** In-place array methods change their receiver, as in Rust (FIELDPOP-1, ARRAYMUT-1). `v.sort()`, `reverse`, `append`, `extend`, `insert`, `remove`, `clear`, `truncate`, `dedup`, `resize` and @@ -62,16 +67,23 @@ Behaviour changes a program can observe are marked **(behaviour)**. and return what Rust returns: nil, the removed element for `remove`, and the element or nil for `pop`. Before, `sort` and `reverse` returned a copy and left the variable unchanged, a field's `push`/`pop` was lost, and `insert`, - `remove`, `clear` and `truncate` were unknown. `sorted`/`reversed` still + `remove`, `clear` and `truncate` were unknown. A nested field + (`self.inner.items.push(x)`, `o.a.b.sort()`) is written back too. + `sorted`/`reversed` still return a copy. `a.append(b)` does not empty `b`. - A `let`-bound array literal that is later grown (`push`, `extend`, `insert`, …) or annotated `Vec<…>` transpiles to `vec![…]`, and so does an array literal for a `Vec<…>` struct field (LETVEC-1, VECLIT-1). Before, - `let mut v = [1, 2]; v.push(3)` failed rustc (E0599). `append` transpiles to - `extend`. + `let mut v = [1, 2]; v.push(3)` failed rustc (E0599). The scan follows + scope: growth after a shadowing `let v = …`, or inside a closure or block + that re-binds `v`, does not count for the outer `v`. +- **(behaviour)** `a.append(b)` transpiles to `a.extend_from_slice(&b)`, so `b` + (a `Vec` or an array) stays usable afterwards, as in the interpreter. Before, + it transpiled to `a.push(b)`. - A keyword can name a method or field after `.` or `?.` (`v.extend(x)`, `o.type`), and a struct can declare it (`struct W { type: i32 }`). A Rust - reserved word is emitted as `r#type` (EXTENDKW-1, RESFIELD-1). + reserved word is emitted as `r#type` (EXTENDKW-1, RESFIELD-1). A `let` + named `self`, `Self`, `super` or `crate` no longer panics the transpiler. - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/tests/transpiler_scope_global_mut.rs b/tests/transpiler_scope_global_mut.rs index c20a498e0..946174445 100644 --- a/tests/transpiler_scope_global_mut.rs +++ b/tests/transpiler_scope_global_mut.rs @@ -81,16 +81,20 @@ increment(); println!("{}", counter); "#; - let temp_file = NamedTempFile::new().unwrap(); - let ruchy_path = temp_file.path().with_extension("ruchy"); + // RHLGA-1 F8: compile inside a temp dir, so the default `a.out` (and any + // other compiler output) lands there and never in the repository. + let dir = tempfile::tempdir().unwrap(); + let ruchy_path = dir.path().join("prog.ruchy"); fs::write(&ruchy_path, code).unwrap(); // CRITICAL: Must compile successfully let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("ruchy"); - cmd.arg("compile").arg(&ruchy_path).assert().success(); // ❌ This will FAIL until we fix the bug - - // Clean up - let _ = fs::remove_file(&ruchy_path); + cmd.current_dir(dir.path()) + .arg("compile") + .arg(&ruchy_path) + .assert() + .success(); + assert!(dir.path().join("a.out").exists(), "a.out in the temp dir"); } /// Test 3: Full execution via ruchy run command From ffda891a461bff236de0ca3545de1a9bcb75e03c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:11:35 +0200 Subject: [PATCH 058/106] RHLGA-1: F6 keeps eval_field_assign variable-only; field chains write back in dispatch The first F6 cut made eval_field_assign recurse, which also turned direct nested assignment (`obj.inner.z = 5`) from an error into a write. Two lib unit tests in interpreter_control_flow.rs (outside this ticket's scope) assert that error, so the gate went red (test_eval_assign_complex_field_target, test_eval_compound_assign_complex_field_access_error). - eval_field_assign is variable-rooted again; its per-variant update moves to interpreter_methods_dispatch::with_field, shared by both paths. - try_field_array_mutation writes a nested field back through write_field_chain, which rebuilds each parent and ends in eval_field_assign. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2 +- src/runtime/interpreter_control_flow.rs | 64 +++------------------ src/runtime/interpreter_methods_dispatch.rs | 58 ++++++++++++++++++- 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index dd60c8de4..bc37988f1 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T07:15:02.015379570Z", + "created_at": "2026-09-24T07:15:03.812620091Z", "git_context": null, "files": { "./build.rs": { diff --git a/src/runtime/interpreter_control_flow.rs b/src/runtime/interpreter_control_flow.rs index eaa0a3470..c74511141 100644 --- a/src/runtime/interpreter_control_flow.rs +++ b/src/runtime/interpreter_control_flow.rs @@ -487,69 +487,21 @@ impl Interpreter { /// Evaluate field assignment: `obj.field = value`. /// /// Handles Object, ObjectMut, Class, and Struct field updates. - /// `object` is a variable or a field chain rooted at one (`o.inner.items = …`): a value-typed parent is - /// rebuilt with the new field and written back to its own parent in turn. pub(crate) fn eval_field_assign( &mut self, object: &Expr, field: &str, val: Value, ) -> Result { - /// `obj` with `field` set to `val`: a copy for value types, the same - /// shared value (updated in place) for `ObjectMut` and `Class`. - fn with_field(obj: Value, field: &str, val: Value) -> Result { - match obj { - Value::Object(map) => { - let mut new_map = (*map).clone(); - new_map.insert(field.to_string(), val); - Ok(Value::Object(Arc::new(new_map))) - } - Value::ObjectMut(ref cell) => { - cell.lock() - .expect("Mutex poisoned: object lock is corrupted") - .insert(field.to_string(), val); - Ok(obj) - } - Value::Class { ref fields, .. } => { - fields - .write() - .expect("RwLock poisoned: class fields lock is corrupted") - .insert(field.to_string(), val); - Ok(obj) - } - Value::Struct { name, fields } => { - let mut new_fields = (*fields).clone(); - new_fields.insert(field.to_string(), val); - Ok(Value::Struct { - name, - fields: Arc::new(new_fields), - }) - } - _ => Err(InterpreterError::RuntimeError(format!( - "Cannot access field '{field}' on non-object" - ))), - } - } - match &object.kind { - ExprKind::Identifier(obj_name) => { - let obj = self.lookup_variable(obj_name)?; - let updated = with_field(obj, field, val.clone())?; - self.set_variable(obj_name, updated); - Ok(val) - } - ExprKind::FieldAccess { - object: parent, - field: parent_field, - } => { - let obj = self.eval_expr(object)?; - let updated = with_field(obj, field, val.clone())?; - self.eval_field_assign(parent, parent_field, updated)?; - Ok(val) - } - _ => Err(InterpreterError::RuntimeError( + let ExprKind::Identifier(obj_name) = &object.kind else { + return Err(InterpreterError::RuntimeError( "Complex field access not supported".to_string(), - )), - } + )); + }; + let obj = self.lookup_variable(obj_name)?; + let updated = super::interpreter_methods_dispatch::with_field(obj, field, val.clone())?; + self.set_variable(obj_name, updated); + Ok(val) } pub(crate) fn eval_index_assign( diff --git a/src/runtime/interpreter_methods_dispatch.rs b/src/runtime/interpreter_methods_dispatch.rs index 8611f7397..eff2c8acf 100644 --- a/src/runtime/interpreter_methods_dispatch.rs +++ b/src/runtime/interpreter_methods_dispatch.rs @@ -57,6 +57,42 @@ fn is_field_chain_rooted(expr: &Expr) -> bool { } } +/// RHLGA-1 F6: `obj` with `field` set to `val`: a copy for value types, the same +/// shared value (updated in place) for `ObjectMut` and `Class`. +pub(crate) fn with_field(obj: Value, field: &str, val: Value) -> Result { + match obj { + Value::Object(map) => { + let mut new_map = (*map).clone(); + new_map.insert(field.to_string(), val); + Ok(Value::Object(Arc::new(new_map))) + } + Value::ObjectMut(ref cell) => { + cell.lock() + .expect("Mutex poisoned: object lock is corrupted") + .insert(field.to_string(), val); + Ok(obj) + } + Value::Class { ref fields, .. } => { + fields + .write() + .expect("RwLock poisoned: class fields lock is corrupted") + .insert(field.to_string(), val); + Ok(obj) + } + Value::Struct { name, fields } => { + let mut new_fields = (*fields).clone(); + new_fields.insert(field.to_string(), val); + Ok(Value::Struct { + name, + fields: Arc::new(new_fields), + }) + } + _ => Err(InterpreterError::RuntimeError(format!( + "Cannot access field '{field}' on non-object" + ))), + } +} + impl Interpreter { /// FIELDPOP-1 / ARRAYMUT-1: an in-place array method on a field /// (`self.items.pop()`, `s.items.sort()`, `o.items.insert(0, x)`) behaves @@ -82,10 +118,30 @@ impl Interpreter { return Ok(None); }; let (items, result) = self.apply_in_place_call(&arr, method, args)?; - self.eval_field_assign(object, field, Value::Array(Arc::from(items)))?; + self.write_field_chain(object, field, Value::Array(Arc::from(items)))?; Ok(Some(result)) } + /// RHLGA-1 F6: write `val` to `object.field` where `object` is a field + /// chain rooted at a variable: each value-typed parent is rebuilt with + /// the new field and written to its own parent, ending at the variable. + fn write_field_chain( + &mut self, + object: &Expr, + field: &str, + val: Value, + ) -> Result<(), InterpreterError> { + let ExprKind::FieldAccess { + object: parent, + field: parent_field, + } = &object.kind + else { + return self.eval_field_assign(object, field, val).map(|_| ()); + }; + let updated = with_field(self.eval_expr(object)?, field, val)?; + self.write_field_chain(parent, parent_field, updated) + } + /// ARRAYMUT-1: an in-place array method on a local array variable /// (`v.push(x)`, `v.pop()`, `v.sort()`, `v.remove(i)`, …) returns the /// method's own result and rebinds the variable to the changed array. From 6027bf861297bbfccc8966b22cfae3811b1dfbcc Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:24:58 +0200 Subject: [PATCH 059/106] RHLGA-1: NESTASSIGN-1 nested field assignment writes back through the chain - eval_field_assign accepts a field chain rooted at a variable (o.inner.z = 5, self.a.b = x, o.items[0].z = 5); each value-typed parent is rebuilt and written to its own parent (write_back / eval_element_assign) - compound assignment (o.inner.z += 1) reuses the same writer - write_field_chain in interpreter_methods_dispatch.rs removed; in-place array methods on fields call eval_field_assign (one chain writer) - lib tests that asserted "Complex field access not supported" now assert the write-back; new error tests for a field on a non-object and a literal root - tests/nestassign_1_nested_field_assign.rs: 7 tests, rustc differential Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/runtime/interpreter_control_flow.rs | 169 +++++++++++++--- src/runtime/interpreter_methods_dispatch.rs | 22 +-- tests/nestassign_1_nested_field_assign.rs | 204 ++++++++++++++++++++ 3 files changed, 343 insertions(+), 52 deletions(-) create mode 100644 tests/nestassign_1_nested_field_assign.rs diff --git a/src/runtime/interpreter_control_flow.rs b/src/runtime/interpreter_control_flow.rs index c74511141..48819b923 100644 --- a/src/runtime/interpreter_control_flow.rs +++ b/src/runtime/interpreter_control_flow.rs @@ -423,7 +423,7 @@ impl Interpreter { } ExprKind::FieldAccess { object, field } => self.eval_field_assign(object, field, val), // BUG-003: Support array index assignment (arr[i] = value) - ExprKind::IndexAccess { object, index } => self.eval_index_assign(object, index, val), + ExprKind::IndexAccess { object, index } => self.eval_element_assign(object, index, val), _ => Err(InterpreterError::RuntimeError( "Invalid assignment target".to_string(), )), @@ -487,20 +487,86 @@ impl Interpreter { /// Evaluate field assignment: `obj.field = value`. /// /// Handles Object, ObjectMut, Class, and Struct field updates. + /// + /// NESTASSIGN-1: `object` may be a chain rooted at a variable + /// (`o.inner.z = 5`, `self.a.b = x`, `o.items[0].z = 5`): each value-typed + /// parent is rebuilt with the new field or element and written to its own + /// parent, ending at the variable. This is the single chain writer; the + /// in-place array methods on fields (`s.a.items.push(x)`) use it too. pub(crate) fn eval_field_assign( &mut self, object: &Expr, field: &str, val: Value, ) -> Result { - let ExprKind::Identifier(obj_name) = &object.kind else { + let parent = match &object.kind { + ExprKind::Identifier(obj_name) => self.lookup_variable(obj_name)?, + ExprKind::FieldAccess { .. } | ExprKind::IndexAccess { .. } => { + self.eval_expr(object)? + } + _ => { + return Err(InterpreterError::RuntimeError( + "Invalid field assignment target".to_string(), + )) + } + }; + let updated = super::interpreter_methods_dispatch::with_field(parent, field, val.clone())?; + self.write_back(object, updated)?; + Ok(val) + } + + /// NESTASSIGN-1: store `updated` at the place `target` names: a variable, + /// a field of a chain, or an element of an array. + fn write_back(&mut self, target: &Expr, updated: Value) -> Result { + match &target.kind { + ExprKind::Identifier(name) => { + self.set_variable(name, updated.clone()); + Ok(updated) + } + ExprKind::FieldAccess { object, field } => { + self.eval_field_assign(object, field, updated) + } + ExprKind::IndexAccess { object, index } => { + self.eval_element_assign(object, index, updated) + } + _ => Err(InterpreterError::RuntimeError( + "Invalid assignment target".to_string(), + )), + } + } + + /// Element assignment `object[index] = val`. An array reached through a + /// field chain (`o.items[0] = x`) is rebuilt and written back through the + /// chain; every other object uses [`Self::eval_index_assign`]. + fn eval_element_assign( + &mut self, + object: &Expr, + index: &Expr, + val: Value, + ) -> Result { + let ExprKind::FieldAccess { .. } = &object.kind else { + return self.eval_index_assign(object, index, val); + }; + let Value::Array(arr) = self.eval_expr(object)? else { return Err(InterpreterError::RuntimeError( - "Complex field access not supported".to_string(), + "Cannot index non-array value".to_string(), )); }; - let obj = self.lookup_variable(obj_name)?; - let updated = super::interpreter_methods_dispatch::with_field(obj, field, val.clone())?; - self.set_variable(obj_name, updated); + let Value::Integer(i) = self.eval_expr(index)? else { + return Err(InterpreterError::RuntimeError( + "Array index must be an integer".to_string(), + )); + }; + let mut items = arr.to_vec(); + let len = items.len(); + let slot = usize::try_from(i).ok().and_then(|i| items.get_mut(i)); + let Some(slot) = slot else { + return Err(InterpreterError::RuntimeError(format!( + "Index {i} out of bounds for array of length {len}" + ))); + }; + *slot = val.clone(); + self.write_back(object, Value::Array(Arc::from(items)))?; Ok(val) } @@ -1666,7 +1732,7 @@ mod tests { } #[test] - fn test_eval_assign_complex_field_target() { + fn test_eval_assign_nested_field_target_writes_back() { let mut interp = make_interpreter(); // Set up nested object @@ -1676,22 +1742,72 @@ mod tests { outer.insert("inner".to_string(), Value::Object(Arc::new(inner))); interp.set_variable("obj", Value::Object(Arc::new(outer))); - // obj.inner.z = 5 -- complex target (object is a FieldAccess, not Identifier) - let target = make_expr(ExprKind::FieldAccess { + // NESTASSIGN-1: obj.inner.z = 5 writes through `inner` back to `obj` + let target = nested_z_target("obj"); + let value = make_expr(ExprKind::Literal(Literal::Integer(5, None))); + + let result = interp.eval_assign(&target, &value); + assert_eq!(result.expect("nested field assign"), Value::Integer(5)); + assert_eq!(nested_z(&mut interp), Value::Integer(5)); + } + + /// `.inner.z` as an assignment target. + fn nested_z_target(root: &str) -> Expr { + make_expr(ExprKind::FieldAccess { object: Box::new(make_expr(ExprKind::FieldAccess { - object: Box::new(make_expr(ExprKind::Identifier("obj".to_string()))), + object: Box::new(make_expr(ExprKind::Identifier(root.to_string()))), field: "inner".to_string(), })), field: "z".to_string(), - }); + }) + } + + /// The current value of `obj.inner.z`. + fn nested_z(interp: &mut Interpreter) -> Value { + let target = nested_z_target("obj"); + interp.eval_expr(&target).expect("obj.inner.z is readable") + } + + #[test] + fn test_eval_assign_field_on_non_object_is_error() { + let mut interp = make_interpreter(); + let mut outer = HashMap::new(); + outer.insert("inner".to_string(), Value::Integer(3)); + interp.set_variable("obj", Value::Object(Arc::new(outer))); + + // obj.inner.z = 5 where obj.inner is an integer let value = make_expr(ExprKind::Literal(Literal::Integer(5, None))); + let err = interp + .eval_assign(&nested_z_target("obj"), &value) + .expect_err("field of an integer is not assignable"); + assert!(err.to_string().contains("non-object"), "{err}"); + // the failed assignment leaves obj unchanged + let inner = make_expr(ExprKind::FieldAccess { + object: Box::new(make_expr(ExprKind::Identifier("obj".to_string()))), + field: "inner".to_string(), + }); + assert_eq!( + interp.eval_expr(&inner).expect("obj.inner"), + Value::Integer(3) + ); + } - let result = interp.eval_assign(&target, &value); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Complex field access not supported")); + #[test] + fn test_eval_assign_field_on_call_result_is_error() { + let mut interp = make_interpreter(); + // (1).z = 5: the object is neither a variable nor a chain rooted at one + let target = make_expr(ExprKind::FieldAccess { + object: Box::new(make_expr(ExprKind::Literal(Literal::Integer(1, None)))), + field: "z".to_string(), + }); + let value = make_expr(ExprKind::Literal(Literal::Integer(5, None))); + let err = interp + .eval_assign(&target, &value) + .expect_err("a literal has no assignable field"); + assert!( + err.to_string().contains("Invalid field assignment target"), + "{err}" + ); } #[test] @@ -1777,7 +1893,7 @@ mod tests { } #[test] - fn test_eval_compound_assign_complex_field_access_error() { + fn test_eval_compound_assign_nested_field() { let mut interp = make_interpreter(); // Set up nested object @@ -1787,22 +1903,13 @@ mod tests { outer.insert("inner".to_string(), Value::Object(Arc::new(inner))); interp.set_variable("obj", Value::Object(Arc::new(outer))); - // obj.inner.z += 1 -- complex field access not supported - let target = make_expr(ExprKind::FieldAccess { - object: Box::new(make_expr(ExprKind::FieldAccess { - object: Box::new(make_expr(ExprKind::Identifier("obj".to_string()))), - field: "inner".to_string(), - })), - field: "z".to_string(), - }); + // NESTASSIGN-1: obj.inner.z += 1 reads and writes through the chain + let target = nested_z_target("obj"); let value = make_expr(ExprKind::Literal(Literal::Integer(1, None))); let result = interp.eval_compound_assign(&target, AstBinaryOp::Add, &value); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Complex field access not supported")); + assert_eq!(result.expect("nested compound assign"), Value::Integer(6)); + assert_eq!(nested_z(&mut interp), Value::Integer(6)); } #[test] diff --git a/src/runtime/interpreter_methods_dispatch.rs b/src/runtime/interpreter_methods_dispatch.rs index eff2c8acf..6602d88a1 100644 --- a/src/runtime/interpreter_methods_dispatch.rs +++ b/src/runtime/interpreter_methods_dispatch.rs @@ -118,30 +118,10 @@ impl Interpreter { return Ok(None); }; let (items, result) = self.apply_in_place_call(&arr, method, args)?; - self.write_field_chain(object, field, Value::Array(Arc::from(items)))?; + self.eval_field_assign(object, field, Value::Array(Arc::from(items)))?; Ok(Some(result)) } - /// RHLGA-1 F6: write `val` to `object.field` where `object` is a field - /// chain rooted at a variable: each value-typed parent is rebuilt with - /// the new field and written to its own parent, ending at the variable. - fn write_field_chain( - &mut self, - object: &Expr, - field: &str, - val: Value, - ) -> Result<(), InterpreterError> { - let ExprKind::FieldAccess { - object: parent, - field: parent_field, - } = &object.kind - else { - return self.eval_field_assign(object, field, val).map(|_| ()); - }; - let updated = with_field(self.eval_expr(object)?, field, val)?; - self.write_field_chain(parent, parent_field, updated) - } - /// ARRAYMUT-1: an in-place array method on a local array variable /// (`v.push(x)`, `v.pop()`, `v.sort()`, `v.remove(i)`, …) returns the /// method's own result and rebinds the variable to the changed array. diff --git a/tests/nestassign_1_nested_field_assign.rs b/tests/nestassign_1_nested_field_assign.rs new file mode 100644 index 000000000..951523da7 --- /dev/null +++ b/tests/nestassign_1_nested_field_assign.rs @@ -0,0 +1,204 @@ +//! NESTASSIGN-1: assignment to a nested field (`o.inner.z = 5`, +//! `o.inner.z += 1`, `self.a.b = x`, `o.items[0].z = 5`) writes the value +//! through every value-typed parent back to the root variable. It used to fail +//! with "Runtime error: Complex field access not supported" because field +//! assignment only accepted a variable as the object. +//! +//! Each program is also transpiled and compiled with rustc; the compiled +//! binary must print the same output as the interpreter. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(30)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn interpret(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + stdout_of(cmd, "ruchy run") +} + +fn run_file(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + interpret(dir.path(), src) +} + +fn transpile(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, "ruchy transpile") +} + +fn rustc_run(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("main.rs"); + let bin = dir.join("prog_bin"); + std::fs::write(&main_rs, rust).expect("write main.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "transpiled binary") +} + +/// Interpret, then transpile + rustc + run; both must print `expected`. +fn assert_both(src: &str, expected: &str) { + assert_eq!( + run_file(src), + expected, + "interpreter output\nsource:\n{src}" + ); + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), src); + let actual = rustc_run(dir.path(), &rust); + assert_eq!( + actual, expected, + "transpiled Rust output\nsource:\n{src}\nrust:\n{rust}" + ); +} + +const NEST: &str = "struct In { z: i32 }\nstruct Out { inner: In }\n"; + +#[test] +fn test_nestassign_1_01_two_level_assign() { + let src = format!( + "{NEST}fun main() {{ + let mut o = Out {{ inner: In {{ z: 1 }} }} + o.inner.z = 5 + println(o.inner.z) +}} +" + ); + assert_both(&src, "5\n"); +} + +#[test] +fn test_nestassign_1_02_two_level_compound_assign() { + let src = format!( + "{NEST}fun main() {{ + let mut o = Out {{ inner: In {{ z: 1 }} }} + o.inner.z += 1 + o.inner.z *= 10 + println(o.inner.z) +}} +" + ); + assert_both(&src, "20\n"); +} + +#[test] +fn test_nestassign_1_03_three_level_assign() { + let src = "struct C { v: i32 } +struct B { c: C } +struct A { b: B } +fun main() { + let mut a = A { b: B { c: C { v: 1 } } } + a.b.c.v = 7 + a.b.c.v += 1 + println(a.b.c.v) +} +"; + assert_both(src, "8\n"); +} + +#[test] +fn test_nestassign_1_04_self_nested_assign_in_method() { + let src = format!( + "{NEST}impl Out {{ + fun set(&mut self, x: i32) {{ + self.inner.z = x + }} + fun bump(&mut self) {{ + self.inner.z += 1 + }} +}} +fun main() {{ + let mut o = Out {{ inner: In {{ z: 1 }} }} + o.set(40) + o.bump() + println(o.inner.z) +}} +" + ); + assert_both(&src, "41\n"); +} + +#[test] +fn test_nestassign_1_05_sibling_fields_untouched() { + let src = "struct In { z: i32, w: i32 } +struct Out { inner: In, k: i32 } +fun main() { + let mut o = Out { inner: In { z: 1, w: 2 }, k: 3 } + o.inner.z = 9 + println(o.inner.z) + println(o.inner.w) + println(o.k) +} +"; + assert_both(src, "9\n2\n3\n"); +} + +/// Interpreter only: the transpiler emits `o.items[0 as usize].clone().z = 5`, +/// which assigns to a temporary (a separate transpiler defect, reported with +/// this ticket), so the rustc differential is not asserted here. +#[test] +fn test_nestassign_1_06_index_in_chain_assign() { + let src = "struct In { z: i32 } +struct Out { items: Vec } +fun main() { + let mut o = Out { items: vec![In { z: 1 }, In { z: 2 }] } + o.items[0].z = 5 + o.items[0].z += 1 + o.items[1] = In { z: 7 } + println(o.items[0].z) + println(o.items[1].z) +} +"; + assert_eq!(run_file(src), "6\n7\n"); +} + +#[test] +fn test_nestassign_1_07_field_on_non_object_is_an_error() { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source( + dir.path(), + "fun main() {\n let mut n = 3\n n.z = 5\n println(n)\n}\n", + ); + let out = ruchy().arg("run").arg(file).output().expect("spawn ruchy"); + assert!( + !out.status.success(), + "assigning a field on an integer must fail" + ); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("non-object"), + "error names the non-object target: {stderr}" + ); +} From 039d6e1b43b6eb399f0d0f183f332f356f5a2759 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:25:36 +0200 Subject: [PATCH 060/106] RHLGA-1: walk tests use a private fixture instead of the shared /tmp Root cause: test_walk_temp_dir, test_walk_with_options_{min,max}_depth and test_walk_parallel walked env_temp_dir(), i.e. the whole host /tmp (over two million entries here, mutated concurrently by other tests and sessions). Run time and memory scaled with the host, not the code: test_walk_parallel exceeded nextest's 120s limit on a 32-core host, and the assertions were guarded by `if let Some` so they checked nothing. Each test now walks a tempdir fixture (root, a.txt, sub, sub/b.txt) and asserts exact counts, names and sizes. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 148 +++++++++--------- .../tests/eval_builtin_tests_part2.rs | 62 ++++++-- 2 files changed, 119 insertions(+), 91 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index bc37988f1..51d121b21 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T07:15:03.812620091Z", + "created_at": "2026-09-24T07:41:00.863835752Z", "git_context": null, "files": { "./build.rs": { @@ -36973,48 +36973,48 @@ }, "./src/runtime/interpreter_control_flow.rs": { "content_hash": [ - 45, - 184, - 182, - 50, - 164, - 125, - 45, - 109, - 175, - 163, - 101, + 122, + 207, + 75, + 215, + 173, + 2, + 94, 221, - 42, - 224, - 76, - 12, - 141, - 105, + 83, + 180, + 203, + 251, + 116, + 248, + 136, + 73, + 48, 59, - 43, - 196, - 183, - 7, - 85, - 193, - 161, - 76, + 236, + 55, + 111, + 143, + 154, + 123, + 248, 186, - 87, - 216, - 148, - 210 + 198, + 250, + 234, + 206, + 180, + 48 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 13.990285, + "duplication_ratio": 14.044564, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 73.99028, + "total": 74.04456, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -37026,15 +37026,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 187 duplicate code patterns" + "issue": "Found 184 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.009715, + "amount": 5.9554358, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 30.0%" + "issue": "Code duplication: 29.8%" }, { "source_metric": "StructuralComplexity", @@ -37050,7 +37050,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 114" + "issue": "High cyclomatic complexity: 113" } ], "critical_defects_count": 0, @@ -37542,48 +37542,48 @@ }, "./src/runtime/interpreter_methods_dispatch.rs": { "content_hash": [ - 44, - 67, - 201, - 46, + 167, + 117, + 189, + 5, + 172, + 159, + 110, 166, - 220, - 67, - 243, - 254, - 157, - 91, - 127, - 212, - 197, - 23, - 206, - 31, - 131, - 181, - 163, - 68, - 158, - 121, - 103, - 130, - 26, - 77, - 181, + 101, + 231, + 136, + 207, + 64, + 180, + 199, + 175, + 60, + 207, + 80, + 232, + 253, + 202, 77, - 243, - 5, - 217 + 118, + 172, + 185, + 185, + 220, + 61, + 147, + 196, + 224 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.305085, + "duplication_ratio": 14.38992, "coupling_score": 11.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 70.305084, + "total": 70.38992, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -37595,15 +37595,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 166 duplicate code patterns" + "issue": "Found 173 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.694915, + "amount": 5.61008, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 28.5%" + "issue": "Code duplication: 28.1%" }, { "source_metric": "StructuralComplexity", @@ -37611,7 +37611,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 61" + "issue": "High cognitive complexity: 62" }, { "source_metric": "StructuralComplexity", @@ -37619,7 +37619,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 78" + "issue": "High cyclomatic complexity: 83" }, { "source_metric": "Coupling", @@ -45046,7 +45046,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.288246, + "avg_score": 90.288506, "grade_distribution": { "A+": 195, "A": 175, diff --git a/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part2.rs b/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part2.rs index 15a23e41f..f52766376 100644 --- a/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part2.rs +++ b/src/runtime/eval_builtin_tests/tests/eval_builtin_tests_part2.rs @@ -662,13 +662,34 @@ fn test_fs_metadata_temp_dir() { } } +/// RHLGA-1: the walk tests used to walk `env_temp_dir()` — the whole shared +/// `/tmp` (millions of entries on a busy host, mutated concurrently by other +/// tests), so their time and memory scaled with the host, not the code. They +/// walk a private fixture instead: `root/a.txt`, `root/sub/b.txt`, i.e. four +/// entries at depths 0, 1, 1, 2. +fn walk_fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("a.txt"), "a").expect("write a.txt"); + std::fs::create_dir(dir.path().join("sub")).expect("mkdir sub"); + std::fs::write(dir.path().join("sub").join("b.txt"), "bb").expect("write b.txt"); + dir +} + +/// Evaluate `code` with `ROOT` replaced by the fixture path as a string literal. +fn eval_on_fixture(code: &str) -> String { + let dir = walk_fixture(); + let root = format!("{:?}", dir.path().display().to_string()); + eval(&code.replace("ROOT", &root)) +} + #[test] fn test_walk_temp_dir() { - let result = try_eval("walk(env_temp_dir())"); - if let Some(r) = result { - // Should return array of FileEntry objects - assert!(r.contains("[") || r.contains("path")); - } + assert_eq!(eval_on_fixture("walk(ROOT).len()"), "4"); + let names = eval_on_fixture("walk(ROOT).map(|e| e.name)"); + assert!( + names.contains("a.txt") && names.contains("b.txt"), + "{names}" + ); } #[test] @@ -914,27 +935,34 @@ fn test_append_file() { // === Walk With Options === #[test] fn test_walk_with_options_max_depth() { - let result = try_eval("walk_with_options(env_temp_dir(), {max_depth: 1})"); - if let Some(r) = result { - assert!(r.contains("[") || r.contains("path")); - } + assert_eq!( + eval_on_fixture("walk_with_options(ROOT, {max_depth: 1}).len()"), + "3" + ); } #[test] fn test_walk_with_options_min_depth() { - let result = try_eval("walk_with_options(env_temp_dir(), {min_depth: 0})"); - if let Some(r) = result { - assert!(r.contains("[") || r.contains("path")); - } + assert_eq!( + eval_on_fixture("walk_with_options(ROOT, {min_depth: 0}).len()"), + "4" + ); + assert_eq!( + eval_on_fixture("walk_with_options(ROOT, {min_depth: 1}).len()"), + "3" + ); + assert_eq!( + eval_on_fixture("walk_with_options(ROOT, {min_depth: 2}).len()"), + "1" + ); } // === Walk Parallel === #[test] fn test_walk_parallel() { - let result = try_eval("walk_parallel(env_temp_dir())"); - if let Some(r) = result { - assert!(r.contains("[") || r.contains("path")); - } + assert_eq!(eval_on_fixture("walk_parallel(ROOT).len()"), "4"); + let sizes = eval_on_fixture("walk_parallel(ROOT).filter(|e| e.is_file).map(|e| e.size)"); + assert!(sizes.contains('1') && sizes.contains('2'), "{sizes}"); } // === Compute Hash === From a8de0355630b889a27881257de785445c80dd7e1 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:25:58 +0200 Subject: [PATCH 061/106] RHLGA-1: UNITRET-1 unit-returning method tails infer no return type - is_void_expression treats a MethodCall whose Rust counterpart returns () (push, insert, extend, clear, sort, reverse, truncate, retain, dedup, swap, ...) as void, so `fun f(xs: &mut Vec) { xs.push(1) }` no longer transpiles to `-> i32` (rustc E0308) - Value-producing tails (arithmetic, x.abs(), xs.len()) keep their inferred return type - tests/unitret_1_unit_body_return.rs: transpile + rustc for unit tails, keep-type cases; lib unit tests for is_unit_method Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 2 +- src/backend/transpiler/function_analysis.rs | 77 ++++++++++ tests/unitret_1_unit_body_return.rs | 161 ++++++++++++++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 tests/unitret_1_unit_body_return.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 51d121b21..c20b3d15d 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T07:41:00.863835752Z", + "created_at": "2026-09-24T07:41:02.588374483Z", "git_context": null, "files": { "./build.rs": { diff --git a/src/backend/transpiler/function_analysis.rs b/src/backend/transpiler/function_analysis.rs index 9dbbd787c..15ea56f46 100644 --- a/src/backend/transpiler/function_analysis.rs +++ b/src/backend/transpiler/function_analysis.rs @@ -68,6 +68,45 @@ pub fn is_void_function_call(expr: &Expr) -> bool { } } +/// UNITRET-1: methods whose Rust counterpart returns `()` (complexity: 1) +/// +/// A function whose tail is one of these calls has no value to return, so no +/// return type is inferred for it. Only methods that return `()` on the +/// standard collections and `String` are listed; `pop`/`remove` return a +/// value and are not. +#[must_use] +pub fn is_unit_method(method: &str) -> bool { + matches!( + method, + "push" + | "push_str" + | "push_back" + | "push_front" + | "insert" + | "extend" + | "extend_from_slice" + | "append" + | "clear" + | "sort" + | "sort_unstable" + | "sort_by" + | "sort_by_key" + | "sort_unstable_by" + | "sort_unstable_by_key" + | "reverse" + | "truncate" + | "retain" + | "dedup" + | "dedup_by_key" + | "resize" + | "swap" + | "fill" + | "reserve" + | "shrink_to_fit" + | "for_each" + ) +} + /// Check if an expression is void (returns unit/nothing) /// /// Recursively analyzes expressions to determine if they produce no value. @@ -78,6 +117,8 @@ pub fn is_void_expression(expr: &Expr) -> bool { ExprKind::Literal(Literal::Unit) => true, // Void function calls ExprKind::Call { .. } if is_void_function_call(expr) => true, + // UNITRET-1: method calls that return unit in Rust (`xs.push(1)`) + ExprKind::MethodCall { method, .. } if is_unit_method(method) => true, // Macro invocations (println!, etc.) ExprKind::MacroInvocation { name, .. } if matches!(name.as_str(), "println" | "print" | "eprintln" | "eprint") => @@ -344,6 +385,42 @@ mod tests { assert!(is_void_function_call(&expr)); } + // ==================== UNITRET-1: unit method tails ==================== + + fn method_call(method: &str) -> Expr { + make_expr(ExprKind::MethodCall { + receiver: Box::new(ident("xs")), + method: method.to_string(), + args: vec![], + }) + } + + #[test] + fn test_unitret_1_unit_method_tail_is_void() { + for method in [ + "push", "insert", "extend", "clear", "sort", "reverse", "truncate", + ] { + assert!(is_void_expression(&method_call(method)), "{method}"); + let block = make_expr(ExprKind::Block(vec![method_call(method)])); + assert!(!has_non_unit_expression(&block), "{method}"); + } + } + + #[test] + fn test_unitret_1_value_method_tail_is_not_void() { + for method in [ + "len", + "pop", + "remove", + "abs", + "get", + "sum", + "unknown_method", + ] { + assert!(!is_void_expression(&method_call(method)), "{method}"); + } + } + // ==================== is_void_expression Tests ==================== #[test] diff --git a/tests/unitret_1_unit_body_return.rs b/tests/unitret_1_unit_body_return.rs new file mode 100644 index 000000000..94a74fd09 --- /dev/null +++ b/tests/unitret_1_unit_body_return.rs @@ -0,0 +1,161 @@ +//! UNITRET-1: a function without a declared return type whose tail +//! expression is a method call that returns unit in Rust (`push`, `clear`, +//! `sort`, ...) must not get an inferred return type. +//! +//! Before the fix `fun f(xs: &mut Vec) { xs.push(1) }` transpiled to +//! `fn f(xs: &mut Vec) -> i32 { xs.push(1) }` and rustc failed with +//! E0308 (expected `i32`, found `()`). +//! +//! Bodies with a value-producing tail keep their inferred return type. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn transpile(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, "ruchy transpile") +} + +/// `ruchy compile` (rustc) the source, then run the binary. +fn compile_and_run(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let bin = dir.path().join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(60)); + stdout_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "compiled binary") +} + +/// Strip whitespace so `fn f ( x ) -> i32` and `fn f(x)->i32` compare equal. +fn squash(s: &str) -> String { + s.chars().filter(|c| !c.is_whitespace()).collect() +} + +/// The transpiled signature of `f`, from `fn f` up to the body brace. +fn signature_of_f(rust: &str) -> String { + let flat = squash(rust); + let start = flat.find("fnf(").expect("fn f in transpiled output"); + let rest = &flat[start..]; + let end = rest.find('{').expect("body brace after fn f"); + rest[..end].to_string() +} + +/// Unit-returning `Vec` method calls used as a function's tail expression. +const UNIT_TAILS: &[&str] = &[ + "xs.push(1)", + "xs.insert(0, 7)", + "xs.extend(vec![4, 5])", + "xs.clear()", + "xs.sort()", + "xs.reverse()", + "xs.truncate(1)", + "xs.retain(|x| *x > 1)", + "xs.dedup()", + "xs.swap(0, 1)", +]; + +#[test] +fn test_unitret_1_unit_method_tail_has_no_return_type() { + for tail in UNIT_TAILS { + let src = format!("fun f(xs: &mut Vec) {{ {tail} }}\n"); + let sig = signature_of_f(&transpile(&src)); + assert!( + !sig.contains("->"), + "`{tail}` returns unit; f must not infer a return type: {sig}" + ); + } +} + +#[test] +fn test_unitret_1_push_tail_compiles_and_runs() { + let src = "fun f(xs: &mut Vec) { xs.push(1) }\n\ + fun main() { let mut v: Vec = vec![]\n f(&mut v)\n f(&mut v)\n println!(\"{}\", v.len()) }\n"; + assert_eq!(compile_and_run(src).trim(), "2"); +} + +#[test] +fn test_unitret_1_every_unit_tail_compiles() { + let fns: String = UNIT_TAILS + .iter() + .enumerate() + .map(|(i, tail)| format!("fun f{i}(xs: &mut Vec) {{ {tail} }}\n")) + .collect(); + // Called last to first so `swap` runs before `clear` empties the Vec. + let calls: String = (0..UNIT_TAILS.len()) + .rev() + .map(|i| format!(" f{i}(&mut v)\n")) + .collect(); + let src = format!( + "{fns}fun main() {{ let mut v: Vec = vec![3, 1, 2]\n{calls} println!(\"{{:?}}\", v) }}\n" + ); + assert_eq!(compile_and_run(&src).trim(), "[7, 4, 5, 1]"); +} + +#[test] +fn test_unitret_1_unit_tail_after_statements() { + let src = "fun add_two(xs: &mut Vec) { let n = xs.len()\n xs.push(1)\n xs.push(2) }\n\ + fun main() { let mut v: Vec = vec![]\n add_two(&mut v)\n println!(\"{:?}\", v) }\n"; + assert_eq!(compile_and_run(src).trim(), "[1, 2]"); +} + +#[test] +fn test_unitret_1_unit_tail_in_if_branches() { + let src = + "fun f(xs: &mut Vec, grow: bool) { if grow { xs.push(9) } else { xs.clear() } }\n"; + let sig = signature_of_f(&transpile(src)); + assert!(!sig.contains("->"), "both branches are unit: {sig}"); +} + +#[test] +fn test_unitret_1_arithmetic_tail_keeps_return_type() { + let src = "fun f(a: i32, b: i32) { a + b }\nfun main() { println!(\"{}\", f(2, 3)) }\n"; + assert!(signature_of_f(&transpile(src)).contains("->i32")); + assert_eq!(compile_and_run(src).trim(), "5"); +} + +#[test] +fn test_unitret_1_value_method_tail_keeps_return_type() { + let src = "fun f(x: i32) { x.abs() }\nfun main() { println!(\"{}\", f(-4)) }\n"; + assert!(signature_of_f(&transpile(src)).contains("->i32")); + assert_eq!(compile_and_run(src).trim(), "4"); +} + +#[test] +fn test_unitret_1_len_tail_keeps_a_return_type() { + let src = "fun f(xs: &Vec) { xs.len() }\n"; + assert!(signature_of_f(&transpile(src)).contains("->")); +} From afa60d6a842e4470055a75aae24601038e84a2aa Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:26:57 +0200 Subject: [PATCH 062/106] RHLGA-1: ENUMFIELDKW-1 keywords name enum struct-variant fields - parse_variant_struct_fields takes the field name through member_names::member_name, as struct fields do (RESFIELD-1): `enum Test { Variant { enum: int } }` no longer fails with "Expected RightBrace, found Enum" (parser_defect_018 proptest) - transpile_enum emits struct-variant field names through safe_ident, so Rust reserved words become raw identifiers (r#enum) - tests/enumfieldkw_1.rs: every member_names keyword as a variant field (check + transpile), one rustc compile with all of them Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 74 ++++---- src/backend/transpiler/types.rs | 2 +- .../parser/expressions_helpers/enums.rs | 18 +- tests/enumfieldkw_1.rs | 168 ++++++++++++++++++ 4 files changed, 217 insertions(+), 45 deletions(-) create mode 100644 tests/enumfieldkw_1.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index c20b3d15d..c79b592b4 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T07:41:02.588374483Z", + "created_at": "2026-09-24T07:41:03.947977547Z", "git_context": null, "files": { "./build.rs": { @@ -5917,48 +5917,48 @@ }, "./src/backend/transpiler/function_analysis.rs": { "content_hash": [ - 20, - 184, - 174, - 47, - 20, - 28, - 62, - 192, - 9, - 78, - 101, - 100, - 74, - 145, - 25, + 83, + 129, + 225, + 103, 227, - 120, - 75, - 26, - 46, - 229, - 168, - 6, - 181, - 136, - 97, + 252, + 251, + 67, + 94, + 237, + 56, + 90, + 143, + 132, + 84, + 42, + 146, + 195, + 147, + 48, 46, - 68, - 190, - 91, - 217, - 210 + 67, + 233, + 162, + 134, + 219, + 195, + 54, + 72, + 124, + 241, + 118 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.246376, + "duplication_ratio": 17.435064, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.95125, + "total": 93.12279, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -5970,15 +5970,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 42 duplicate code patterns" + "issue": "Found 45 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.753623, + "amount": 2.5649352, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.8%" + "issue": "Code duplication: 12.8%" } ], "critical_defects_count": 0, @@ -45046,7 +45046,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.288506, + "avg_score": 90.288826, "grade_distribution": { "A+": 195, "A": 175, diff --git a/src/backend/transpiler/types.rs b/src/backend/transpiler/types.rs index ed75f7e02..649c0e8eb 100644 --- a/src/backend/transpiler/types.rs +++ b/src/backend/transpiler/types.rs @@ -1056,7 +1056,7 @@ impl Transpiler { let field_defs: Vec = fields .iter() .map(|field| { - let field_name = format_ident!("{}", field.name); + let field_name = Transpiler::safe_ident(&field.name); // ENUMFIELDKW-1 let ty = &field.ty; let t = self.transpile_type(ty); let field_type = t.unwrap_or_else(|_| quote! { _ }); diff --git a/src/frontend/parser/expressions_helpers/enums.rs b/src/frontend/parser/expressions_helpers/enums.rs index 3772f3d95..d8eb1c7ad 100644 --- a/src/frontend/parser/expressions_helpers/enums.rs +++ b/src/frontend/parser/expressions_helpers/enums.rs @@ -246,13 +246,17 @@ fn parse_variant_struct_fields(state: &mut ParserState) -> Result { + state.tokens.advance(); + name + } + None => bail!("Expected field name in struct variant"), }; // Expect colon diff --git a/tests/enumfieldkw_1.rs b/tests/enumfieldkw_1.rs new file mode 100644 index 000000000..c5078354e --- /dev/null +++ b/tests/enumfieldkw_1.rs @@ -0,0 +1,168 @@ +//! ENUMFIELDKW-1: a keyword names a field of an enum struct variant, as it +//! names a struct field (RESFIELD-1) and a member after `.` (EXTENDKW-1). +//! +//! Before the fix `enum Test { Variant { enum: int } }` failed to parse with +//! "Expected RightBrace, found Enum" (found by the parser_defect_018 +//! proptest `prop_enum_variant_with_single_field`). +//! +//! Rust reserved words are emitted as raw identifiers (`r#enum`). + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn run_ruchy(sub: &str, src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg(sub).arg(file); + stdout_of(cmd, &format!("ruchy {sub} of:\n{src}\n")) +} + +/// `ruchy compile` (rustc) the source, then run the binary. +fn compile_and_run(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let bin = dir.path().join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(60)); + stdout_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "compiled binary") +} + +fn squash(s: &str) -> String { + s.chars().filter(|c| !c.is_whitespace()).collect() +} + +/// Keywords that are Rust reserved words: transpiled as `r#kw`. +const RUST_RESERVED: &[&str] = &[ + "match", "type", "loop", "impl", "mod", "use", "as", "in", "where", "static", "const", "trait", + "struct", "enum", "try", "yield", "final", "abstract", "override", "async", "return", "break", + "continue", "if", "else", "for", "while", "let", "fn", "pub", "mut", "unsafe", +]; + +/// Ruchy-only keywords: plain Rust identifiers. +const RUCHY_ONLY: &[&str] = &[ + "extend", + "from", + "default", + "module", + "var", + "with", + "import", + "export", + "class", + "handle", + "handler", + "effect", + "property", + "private", + "protected", + "sealed", + "mixin", + "operator", + "interface", + "implements", + "receive", + "spawn", + "actor", + "lazy", + "catch", + "finally", + "throw", + "signal", + "infra", + "requires", + "ensures", + "invariant", + "decreases", + "fun", + "send", + "ask", +]; + +fn all_keywords() -> impl Iterator { + RUST_RESERVED.iter().chain(RUCHY_ONLY.iter()).copied() +} + +fn single_field_enum(kw: &str) -> String { + format!("enum Test {{\n Variant {{ {kw}: i32 }}\n}}\n") +} + +#[test] +fn test_enumfieldkw_1_every_keyword_parses_as_variant_field() { + for kw in all_keywords() { + run_ruchy("check", &single_field_enum(kw)); + } +} + +#[test] +fn test_enumfieldkw_1_reserved_keyword_field_is_raw_ident() { + for kw in RUST_RESERVED { + let rust = squash(&run_ruchy("transpile", &single_field_enum(kw))); + assert!( + rust.contains(&format!("Variant{{r#{kw}:i32}}")), + "`{kw}` must be emitted as r#{kw}: {rust}" + ); + } +} + +#[test] +fn test_enumfieldkw_1_ruchy_only_keyword_field_is_plain_ident() { + for kw in RUCHY_ONLY { + let rust = squash(&run_ruchy("transpile", &single_field_enum(kw))); + assert!( + rust.contains(&format!("Variant{{{kw}:i32}}")), + "`{kw}` must be emitted as a plain identifier: {rust}" + ); + } +} + +#[test] +fn test_enumfieldkw_1_proptest_shape_with_literal() { + let src = "fn main() {\n enum Test {\n Variant { enum: int }\n }\n \ + let x = Test::Variant { enum: 5 }\n println(x)\n}\n"; + run_ruchy("check", src); +} + +#[test] +fn test_enumfieldkw_1_all_keyword_fields_compile() { + let fields: Vec = all_keywords().map(|kw| format!("{kw}: i32")).collect(); + let values: Vec = all_keywords().map(|kw| format!("{kw}: 1")).collect(); + let src = format!( + "enum Test {{\n Variant {{ {} }},\n Empty\n}}\n\ + fun main() {{\n let _t = Test::Variant {{ {} }}\n println!(\"ok\")\n}}\n", + fields.join(", "), + values.join(", ") + ); + assert_eq!(compile_and_run(&src).trim(), "ok"); +} From 609a12cca4ee154664f41ad48a904d0cf1dbdac9 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:46:44 +0200 Subject: [PATCH 063/106] RHLGA-1: IDXASSIGN-1 place-position chains are emitted without clones - `o.items[0].z = 5` transpiled to `o.items[0 as usize].clone().z = 5`, assigning to a temporary; the compiled binary printed the old value. - New Transpiler::transpile_place emits a field/index chain containing an index with no `.clone()` anywhere in it. It is used for the target of `=` and compound assignment, the receiver of a MUTATING_STD_METHODS call (`t.rows[0].vals.push(x)`), and the operand of `&mut`. Chains without an index keep their value emission (module paths, globals unchanged). - Replaces transpile_index_lvalue (subsumed; same output for `v[i][j] = x`); its unit tests now exercise transpile_place, plus a field-of-index case. - Reads (`let s = o.items[0].name`) keep their clone. - tests/idxassign_1_place_targets.rs: 8 transpile+rustc+run cases; nestassign test 06 is now a rustc differential. - Interpreter defects seen, not fixed here (outside transpiler scope): `v[1] += 5` fails with "Invalid compound assignment target", and `t.rows[0].vals.push(9)` is not written back to `t`. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/call_transpilation.rs | 7 +- src/backend/transpiler/expressions.rs | 116 +++++++++--- .../expressions_helpers/async_ops.rs | 7 +- tests/idxassign_1_place_targets.rs | 168 ++++++++++++++++++ tests/nestassign_1_nested_field_assign.rs | 7 +- 5 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 tests/idxassign_1_place_targets.rs diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index beb4bf9aa..7de77ecf2 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -321,7 +321,12 @@ impl Transpiler { } } - let obj_tokens = self.transpile_expr(object)?; + // IDXASSIGN-1: a mutating method's receiver chain is a place + let obj_tokens = if super::mutation_detection::MUTATING_STD_METHODS.contains(&method) { + self.transpile_place(object)? + } else { + self.transpile_expr(object)? + }; let method_ident = Transpiler::safe_ident(method); // RAWIDENT-1 let arg_tokens: Result> = args.iter().map(|a| self.transpile_expr(a)).collect(); let arg_tokens = arg_tokens?; diff --git a/src/backend/transpiler/expressions.rs b/src/backend/transpiler/expressions.rs index e16d5dad9..acc02a9c6 100644 --- a/src/backend/transpiler/expressions.rs +++ b/src/backend/transpiler/expressions.rs @@ -147,14 +147,9 @@ impl Transpiler { // Standard assignment (no deadlock risk) let value_tokens = self.transpile_expr(value)?; - // BUG-003: Handle IndexAccess specially for lvalue (no .clone()) - if let ExprKind::IndexAccess { .. } = &target.kind { - let target_tokens = self.transpile_index_lvalue(target)?; - Ok(quote! { #target_tokens = #value_tokens }) - } else { - let target_tokens = self.transpile_expr(target)?; - Ok(quote! { #target_tokens = #value_tokens }) - } + // BUG-003 / IDXASSIGN-1: the target is a place (no `.clone()` in its chain) + let target_tokens = self.transpile_place(target)?; + Ok(quote! { #target_tokens = #value_tokens }) } /// Check if an expression references a specific variable name @@ -287,23 +282,54 @@ impl Transpiler { } } - /// Transpile `IndexAccess` as an lvalue (no .`clone()`) - /// Handles nested cases like matrix[i][j] - fn transpile_index_lvalue(&self, expr: &Expr) -> Result { + /// IDXASSIGN-1: transpile `expr` in place (lvalue) position. + /// + /// A field/index chain that contains an index (`o.items[0].z`, + /// `a.b[i].c[j].d`, `m[i][j]`) is emitted with no `.clone()` anywhere in + /// it, so the assignment, compound assignment, mutating method call or + /// `&mut` borrow reaches the original element instead of a temporary. + /// Any other expression is transpiled as a value. (complexity: 4) + pub(crate) fn transpile_place(&self, expr: &Expr) -> Result { + if !Self::place_chain_has_index(expr) { + return self.transpile_expr(expr); + } match &expr.kind { ExprKind::IndexAccess { object, index } => { - // Recursively handle nested IndexAccess - let obj_tokens = if matches!(object.kind, ExprKind::IndexAccess { .. }) { - self.transpile_index_lvalue(object)? - } else { - self.transpile_expr(object)? - }; + let obj_tokens = self.transpile_place(object)?; let idx_tokens = self.transpile_expr(index)?; Ok(quote! { #obj_tokens[#idx_tokens as usize] }) } + ExprKind::FieldAccess { object, field } => { + let obj_tokens = self.transpile_place(object)?; + let field_tokens = Self::place_field_tokens(field); + Ok(quote! { #obj_tokens.#field_tokens }) + } _ => self.transpile_expr(expr), } } + + /// True when a field/index chain contains an index access. (complexity: 3) + fn place_chain_has_index(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::IndexAccess { .. } => true, + ExprKind::FieldAccess { object, .. } => Self::place_chain_has_index(object), + _ => false, + } + } + + /// A struct field name or a tuple index (`.0`) of a place. (complexity: 2) + fn place_field_tokens(field: &str) -> TokenStream { + match field.parse::() { + Ok(index) => { + let index = syn::Index::from(index); + quote! { #index } + } + Err(_) => { + let ident = Self::safe_ident(field); + quote! { #ident } + } + } + } /// Transpiles compound assignment /// # Examples /// @@ -346,8 +372,8 @@ impl Transpiler { } } - // Standard compound assignment (non-global) - let target_tokens = self.transpile_expr(target)?; + // Standard compound assignment (non-global); IDXASSIGN-1: target is a place + let target_tokens = self.transpile_place(target)?; let value_tokens = self.transpile_expr(value)?; let op_tokens = Self::get_compound_op_token(op)?; Ok(quote! { #target_tokens #op_tokens #value_tokens }) @@ -1225,7 +1251,7 @@ mod tests { // Test 58: transpile_index_lvalue simple #[test] - fn test_transpile_index_lvalue_simple() { + fn test_transpile_place_index_simple() { let transpiler = Transpiler::new(); let object = Expr::new(ExprKind::Identifier("arr".to_string()), Span::default()); let index = Expr::new( @@ -1239,7 +1265,7 @@ mod tests { }, Span::default(), ); - let result = transpiler.transpile_index_lvalue(&expr); + let result = transpiler.transpile_place(&expr); assert!(result.is_ok()); let code = result.expect("should succeed").to_string(); assert!(code.contains("arr")); @@ -1248,7 +1274,7 @@ mod tests { // Test 59: transpile_index_lvalue nested #[test] - fn test_transpile_index_lvalue_nested() { + fn test_transpile_place_index_nested() { let transpiler = Transpiler::new(); let matrix = Expr::new(ExprKind::Identifier("matrix".to_string()), Span::default()); let i = Expr::new( @@ -1273,18 +1299,58 @@ mod tests { }, Span::default(), ); - let result = transpiler.transpile_index_lvalue(&expr); + let result = transpiler.transpile_place(&expr); assert!(result.is_ok()); let code = result.expect("should succeed").to_string(); assert!(code.contains("matrix")); + assert!(!code.contains("clone"), "{code}"); + } + + // IDXASSIGN-1: a field of an indexed element is a place, not a clone + #[test] + fn test_transpile_place_field_of_index_has_no_clone() { + let transpiler = Transpiler::new(); + let items = Expr::new( + ExprKind::FieldAccess { + object: Box::new(Expr::new( + ExprKind::Identifier("o".to_string()), + Span::default(), + )), + field: "items".to_string(), + }, + Span::default(), + ); + let elem = Expr::new( + ExprKind::IndexAccess { + object: Box::new(items), + index: Box::new(Expr::new( + ExprKind::Literal(Literal::Integer(0, None)), + Span::default(), + )), + }, + Span::default(), + ); + let expr = Expr::new( + ExprKind::FieldAccess { + object: Box::new(elem), + field: "z".to_string(), + }, + Span::default(), + ); + let code = transpiler + .transpile_place(&expr) + .expect("place transpiles") + .to_string() + .replace(' ', ""); + assert_eq!(code, "o.items[0asusize].z"); } // Test 60: transpile_index_lvalue non-index #[test] - fn test_transpile_index_lvalue_non_index() { + fn test_transpile_place_index_non_index() { let transpiler = Transpiler::new(); let expr = Expr::new(ExprKind::Identifier("x".to_string()), Span::default()); - let result = transpiler.transpile_index_lvalue(&expr); + let result = transpiler.transpile_place(&expr); assert!(result.is_ok()); } diff --git a/src/backend/transpiler/expressions_helpers/async_ops.rs b/src/backend/transpiler/expressions_helpers/async_ops.rs index c0a91ea29..927efc6d7 100644 --- a/src/backend/transpiler/expressions_helpers/async_ops.rs +++ b/src/backend/transpiler/expressions_helpers/async_ops.rs @@ -8,7 +8,12 @@ use quote::{format_ident, quote}; impl Transpiler { pub fn transpile_unary(&self, op: UnaryOp, operand: &Expr) -> Result { - let mut operand_tokens = self.transpile_expr(operand)?; + // IDXASSIGN-1: `&mut o.items[0]` borrows the element, not a clone + let mut operand_tokens = if op == UnaryOp::MutableReference { + self.transpile_place(operand)? + } else { + self.transpile_expr(operand)? + }; // TRANSPILENOT-1: `!(b && x)` must not become `!b && x` if Self::unary_operand_needs_parens(operand) { operand_tokens = quote! { (#operand_tokens) }; diff --git a/tests/idxassign_1_place_targets.rs b/tests/idxassign_1_place_targets.rs new file mode 100644 index 000000000..1a6b78df1 --- /dev/null +++ b/tests/idxassign_1_place_targets.rs @@ -0,0 +1,168 @@ +//! IDXASSIGN-1: an expression in place (lvalue) position is emitted as a +//! place, with no `.clone()` anywhere in its chain. Place positions are the +//! target of `=` and of a compound assignment (`+=` etc.), the receiver chain +//! of a mutating method call (`o.rows[0].vals.push(x)`), and the operand of +//! `&mut`. The transpiler used to emit `o.items[0 as usize].clone().z = 5`, +//! which assigns to a temporary: the compiled binary silently printed the old +//! value. +//! +//! Every program is transpiled, compiled with rustc and run; the binary must +//! print the expected output. Where the interpreter runs the program, it must +//! print the same output. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(30)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn transpile(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + stdout_of(cmd, "ruchy transpile") +} + +fn interpret(dir: &Path, src: &str) -> String { + let file = write_source(dir, src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + stdout_of(cmd, "ruchy run") +} + +fn rustc_run(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("main.rs"); + let bin = dir.join("prog_bin"); + std::fs::write(&main_rs, rust).expect("write main.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, &format!("rustc on transpiled Rust:\n{rust}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + stdout_of(run, "transpiled binary") +} + +/// Transpile + rustc + run must print `expected`; returns the Rust source. +fn assert_compiled(src: &str, expected: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let rust = transpile(dir.path(), src); + let actual = rustc_run(dir.path(), &rust); + assert_eq!(actual, expected, "compiled output\nRust:\n{rust}"); + rust +} + +/// Interpreter and compiled binary must both print `expected`. +fn assert_both(src: &str, expected: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!( + interpret(dir.path(), src), + expected, + "interpreter output\nsource:\n{src}" + ); + assert_compiled(src, expected) +} + +const ITEMS: &str = "struct P { z: i32 }\nstruct O { items: Vec

}\n"; + +#[test] +fn test_idxassign_1_01_field_of_indexed_element_assign() { + let src = format!( + "{ITEMS}fun main() {{\n let mut o = O {{ items: vec![P {{ z: 1 }}, P {{ z: 2 }}] }}\n \ + o.items[0].z = 5\n println!(\"{{}} {{}}\", o.items[0].z, o.items[1].z)\n}}\n" + ); + let rust = assert_both(&src, "5 2\n"); + assert!( + !rust.contains(".clone().z ="), + "assignment target must be a place:\n{rust}" + ); +} + +#[test] +fn test_idxassign_1_02_field_of_indexed_element_compound_assign() { + let src = format!( + "{ITEMS}fun main() {{\n let mut o = O {{ items: vec![P {{ z: 1 }}] }}\n \ + o.items[0].z += 3\n o.items[0].z *= 2\n println!(\"{{}}\", o.items[0].z)\n}}\n" + ); + assert_both(&src, "8\n"); +} + +/// Compiled only: the interpreter does not write `push` through an indexed +/// field chain back to `t` (a runtime defect reported with this ticket). +#[test] +fn test_idxassign_1_03_mutating_method_on_indexed_chain() { + let src = "struct Row { vals: Vec }\nstruct T { rows: Vec }\nfun main() {\n \ + let mut t = T { rows: vec![Row { vals: vec![1] }] }\n \ + t.rows[0].vals.push(9)\n t.rows[0].vals.push(4)\n \ + println!(\"{} {}\", t.rows[0].vals.len(), t.rows[0].vals[1])\n}\n"; + assert_compiled(src, "3 9\n"); +} + +#[test] +fn test_idxassign_1_04_mut_ref_argument_of_indexed_element() { + let src = format!( + "{ITEMS}fun bump(p: &mut P) {{\n p.z = p.z + 10\n}}\nfun main() {{\n \ + let mut o = O {{ items: vec![P {{ z: 1 }}] }}\n bump(&mut o.items[0])\n \ + println!(\"{{}}\", o.items[0].z)\n}}\n" + ); + assert_compiled(&src, "11\n"); +} + +#[test] +fn test_idxassign_1_05_nested_index_field_chain() { + let src = "struct D { d: i32 }\nstruct C { c: Vec }\nstruct B { b: Vec }\n\ + fun main() {\n let mut a = B { b: vec![C { c: vec![D { d: 0 }, D { d: 0 }] }] }\n \ + let i = 0\n let j = 1\n a.b[i].c[j].d = 7\n a.b[i].c[j].d += 1\n \ + println!(\"{} {}\", a.b[0].c[0].d, a.b[0].c[1].d)\n}\n"; + assert_both(src, "0 8\n"); +} + +#[test] +fn test_idxassign_1_06_reads_still_compile() { + let src = "struct N { name: String, z: i32 }\nstruct O { items: Vec }\nfun main() {\n \ + let o = O { items: vec![N { name: \"a\".to_string(), z: 3 }] }\n \ + let y = o.items[0].z\n let s = o.items[0].name\n \ + println!(\"{} {} {}\", y, s, o.items[0].name)\n}\n"; + assert_both(src, "3 a a\n"); +} + +/// Compiled only: the interpreter rejects `v[1] += 5` with "Invalid compound +/// assignment target" (a runtime defect reported with this ticket). +#[test] +fn test_idxassign_1_07_plain_index_assign_unchanged() { + let src = "fun main() {\n let mut v = vec![1, 2, 3]\n v[0] = 9\n v[1] += 5\n \ + println!(\"{} {} {}\", v[0], v[1], v[2])\n}\n"; + assert_compiled(src, "9 7 3\n"); +} + +#[test] +fn test_idxassign_1_08_whole_element_replace_in_field_vec() { + let src = format!( + "{ITEMS}fun main() {{\n let mut o = O {{ items: vec![P {{ z: 1 }}, P {{ z: 2 }}] }}\n \ + o.items[1] = P {{ z: 7 }}\n println!(\"{{}} {{}}\", o.items[0].z, o.items[1].z)\n}}\n" + ); + assert_both(&src, "1 7\n"); +} diff --git a/tests/nestassign_1_nested_field_assign.rs b/tests/nestassign_1_nested_field_assign.rs index 951523da7..fc9481946 100644 --- a/tests/nestassign_1_nested_field_assign.rs +++ b/tests/nestassign_1_nested_field_assign.rs @@ -165,9 +165,8 @@ fun main() { assert_both(src, "9\n2\n3\n"); } -/// Interpreter only: the transpiler emits `o.items[0 as usize].clone().z = 5`, -/// which assigns to a temporary (a separate transpiler defect, reported with -/// this ticket), so the rustc differential is not asserted here. +/// IDXASSIGN-1: the transpiler used to emit `o.items[0 as usize].clone().z = 5`, +/// which assigned to a temporary; the compiled binary must now agree. #[test] fn test_nestassign_1_06_index_in_chain_assign() { let src = "struct In { z: i32 } @@ -181,7 +180,7 @@ fun main() { println(o.items[1].z) } "; - assert_eq!(run_file(src), "6\n7\n"); + assert_both(src, "6\n7\n"); } #[test] From 8ec2dd99f1b2106699b4c1b92e3a83944c9b8c1e Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 09:49:02 +0200 Subject: [PATCH 064/106] RHLGA-1: LENRET-1 len()/count() tails infer a usize return type - `fun f(xs: &Vec) { xs.len() }` transpiled to `fn f(..) -> i32` with a usize tail (E0308). - Choice: infer `-> usize`, no cast. The transpiler treats `len()` as a Rust `usize` everywhere else: `v.len()` is emitted unchanged and an unannotated `let n = v.len()` is a usize, so callers (`for i in 0..f(&v)`, indexing) compose with no casts. Casting to i64 (Ruchy `int`) would only move the mismatch to those call sites. - returns_usize (return_type_helpers) checks the final expression for a zero-argument `len`/`count` call; used by both the plain and the lifetime return-type inference paths. - 4 rustc-backed tests in tests/unitret_1_unit_body_return.rs, 2 unit tests. - Seen, not fixed (outside return-type scope): `.count()` emission appends `.iter()`, so `s.chars().count()` becomes `s.chars().iter().count()`; `let n: int = v.len()` emits `let n: i64 = v.len()` with no cast. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 366 +++++++++--------- src/backend/transpiler/function_signature.rs | 4 + src/backend/transpiler/lifetime_helpers.rs | 5 + src/backend/transpiler/return_type_helpers.rs | 42 ++ tests/unitret_1_unit_body_return.rs | 39 ++ 5 files changed, 273 insertions(+), 183 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index c79b592b4..d493bf1bd 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T07:41:03.947977547Z", + "created_at": "2026-09-24T08:01:13.238900929Z", "git_context": null, "files": { "./build.rs": { @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 127, - 172, - 155, - 46, - 2, - 208, - 35, - 32, - 219, - 205, - 180, - 195, - 142, - 126, - 241, - 12, - 239, + 14, + 59, + 115, + 199, + 169, + 38, + 199, + 232, + 104, + 43, + 212, + 183, + 170, + 254, + 4, + 177, 201, - 16, - 215, - 181, - 172, - 154, - 224, - 91, - 144, - 90, - 3, - 248, - 101, - 72, - 114 + 210, + 193, + 121, + 84, + 171, + 50, + 223, + 18, + 225, + 240, + 64, + 237, + 10, + 137, + 109 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.888754, + "duplication_ratio": 15.932611, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.888756, + "total": 75.93261, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3639,15 +3639,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 69 duplicate code patterns" + "issue": "Found 68 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.1112456, + "amount": 4.0673885, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.6%" + "issue": "Code duplication: 20.3%" }, { "source_metric": "StructuralComplexity", @@ -3655,7 +3655,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 68" + "issue": "High cognitive complexity: 69" }, { "source_metric": "StructuralComplexity", @@ -3663,7 +3663,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 68" + "issue": "High cyclomatic complexity: 69" } ], "critical_defects_count": 0, @@ -5317,48 +5317,48 @@ }, "./src/backend/transpiler/expressions.rs": { "content_hash": [ + 160, + 132, 90, + 92, + 85, 75, - 96, - 171, - 202, - 232, - 119, - 123, - 162, - 59, - 125, - 248, - 37, - 116, - 65, - 225, - 184, - 228, - 221, - 174, - 167, - 174, - 8, - 0, - 206, - 81, - 12, - 245, - 158, - 29, - 28, - 107 + 214, + 25, + 109, + 223, + 118, + 69, + 62, + 186, + 85, + 114, + 175, + 138, + 164, + 142, + 145, + 33, + 104, + 203, + 70, + 213, + 36, + 154, + 204, + 79, + 77, + 132 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 13.133758, + "duplication_ratio": 13.222632, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 73.13376, + "total": 73.22263, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -5370,15 +5370,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 126 duplicate code patterns" + "issue": "Found 129 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.8662424, + "amount": 6.7773676, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 34.3%" + "issue": "Code duplication: 33.9%" }, { "source_metric": "StructuralComplexity", @@ -5394,7 +5394,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 112" + "issue": "High cyclomatic complexity: 117" } ], "critical_defects_count": 0, @@ -5491,48 +5491,48 @@ }, "./src/backend/transpiler/expressions_helpers/async_ops.rs": { "content_hash": [ - 139, - 5, - 248, + 202, + 204, + 4, + 143, + 84, + 9, + 31, + 227, + 173, + 165, + 31, + 146, + 29, + 126, + 130, + 67, + 52, + 163, 2, - 65, - 200, - 58, - 177, - 7, - 220, - 228, - 83, - 212, - 38, - 239, - 37, + 243, + 205, + 109, 245, - 60, - 95, - 89, - 19, - 238, - 146, - 142, - 12, - 66, - 172, - 227, - 171, - 244, - 51, - 103 + 189, + 139, + 155, + 110, + 14, + 34, + 152, + 212, + 132 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.724409, + "duplication_ratio": 14.806202, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 99.72441, + "total": 99.8062, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -5544,15 +5544,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 21 duplicate code patterns" + "issue": "Found 23 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.2755904, + "amount": 5.1937985, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 26.4%" + "issue": "Code duplication: 26.0%" } ], "critical_defects_count": 0, @@ -8818,48 +8818,48 @@ }, "./src/backend/transpiler/types.rs": { "content_hash": [ - 252, - 106, - 12, - 56, - 170, - 103, - 11, - 58, - 94, + 79, + 76, + 159, + 178, + 195, + 195, + 23, + 70, + 251, + 187, + 46, + 152, + 122, + 87, + 14, + 73, + 37, 223, - 137, - 186, - 175, + 194, + 83, + 116, + 253, + 178, + 241, + 76, + 223, + 154, 128, - 50, - 177, - 94, - 40, - 27, - 64, - 82, - 201, - 216, - 94, - 170, - 0, - 173, - 168, - 171, - 221, - 172, - 151 + 156, + 155, + 249, + 150 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.695793, + "duplication_ratio": 15.711974, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.69579, + "total": 75.711975, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -8879,15 +8879,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 134 duplicate code patterns" + "issue": "Found 133 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.3042073, + "amount": 4.288026, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 21.5%" + "issue": "Code duplication: 21.4%" }, { "source_metric": "StructuralComplexity", @@ -16069,48 +16069,48 @@ }, "./src/frontend/parser/expressions_helpers/enums.rs": { "content_hash": [ - 71, - 50, - 18, - 214, - 91, - 246, - 138, - 148, - 148, - 253, - 146, - 40, - 73, - 92, - 174, - 107, - 210, - 92, - 230, - 229, - 129, - 129, + 45, + 42, + 216, + 13, + 190, + 13, + 232, + 83, + 137, + 222, + 164, + 207, + 154, + 79, 117, - 145, - 213, - 59, - 115, + 4, + 191, + 61, + 171, + 232, + 44, + 243, + 26, + 228, + 70, + 187, + 184, + 133, + 86, 117, - 181, - 178, - 229, - 61 + 191, + 247 ], "score": { - "structural_complexity": 5.0, + "structural_complexity": 4.5, "semantic_complexity": 20.0, - "duplication_ratio": 17.301981, + "duplication_ratio": 17.339901, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 82.30198, + "total": 81.839905, "grade": "B+", "confidence": 1.0, "language": "Rust", @@ -16126,11 +16126,11 @@ }, { "source_metric": "Duplication", - "amount": 2.6980197, + "amount": 2.6600983, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.5%" + "issue": "Code duplication: 13.3%" }, { "source_metric": "StructuralComplexity", @@ -16142,11 +16142,11 @@ }, { "source_metric": "StructuralComplexity", - "amount": 10.0, + "amount": 10.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 50" + "issue": "High cyclomatic complexity: 51" } ], "critical_defects_count": 0, @@ -45046,7 +45046,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.288826, + "avg_score": 90.28839, "grade_distribution": { "A+": 195, "A": 175, diff --git a/src/backend/transpiler/function_signature.rs b/src/backend/transpiler/function_signature.rs index f9a7c1960..5beff391a 100644 --- a/src/backend/transpiler/function_signature.rs +++ b/src/backend/transpiler/function_signature.rs @@ -77,6 +77,10 @@ impl Transpiler { return Ok(quote! { -> String }); } + if super::return_type_helpers::returns_usize(body) { + return Ok(quote! { -> usize }); + } + // BOOK-COMPAT-017: Check call-site types for numeric return type // Check if function has float arguments - if so, infer f64 return type let has_float_args = diff --git a/src/backend/transpiler/lifetime_helpers.rs b/src/backend/transpiler/lifetime_helpers.rs index 5e846625d..1c310a20b 100644 --- a/src/backend/transpiler/lifetime_helpers.rs +++ b/src/backend/transpiler/lifetime_helpers.rs @@ -154,6 +154,11 @@ impl Transpiler { return Ok(quote! { -> impl Fn(i32) -> i32 }); } + // LENRET-1: a `len()` / `count()` tail is a `usize` + if super::return_type_helpers::returns_usize(body) { + return Ok(quote! { -> usize }); + } + if super::function_analysis::looks_like_numeric_function(name) { return Ok(quote! { -> i32 }); } diff --git a/src/backend/transpiler/return_type_helpers.rs b/src/backend/transpiler/return_type_helpers.rs index 78f3065c7..9970ce67e 100644 --- a/src/backend/transpiler/return_type_helpers.rs +++ b/src/backend/transpiler/return_type_helpers.rs @@ -343,6 +343,19 @@ pub fn expr_creates_or_returns_vec(expr: &Expr, block_exprs: &[Expr]) -> bool { false } +/// LENRET-1: the body's tail is a `len()` / `count()` call. +/// +/// Both are `usize` in the transpiled Rust (an unannotated `let n = v.len()` +/// is a `usize` as well), so the function returns `usize` rather than an +/// integer type its tail cannot produce. (complexity: 2) +#[must_use] +pub fn returns_usize(body: &Expr) -> bool { + get_final_expression(body).is_some_and(|tail| { + matches!(&tail.kind, ExprKind::MethodCall { method, args, .. } + if args.is_empty() && matches!(method.as_str(), "len" | "count")) + }) +} + /// Helper: Get the actual final expression, drilling through Let/Block wrappers /// Complexity: 3 (simple recursive pattern matching) pub fn get_final_expression(expr: &Expr) -> Option<&Expr> { @@ -983,4 +996,33 @@ mod tests { let expr = block(vec![]); assert!(!returns_string(&expr)); } + + fn method(receiver: Expr, name: &str, args: Vec) -> Expr { + make_expr(ExprKind::MethodCall { + receiver: Box::new(receiver), + method: name.to_string(), + args, + }) + } + + // LENRET-1: `len()` / `count()` tails are `usize` + #[test] + fn test_lenret_1_returns_usize_for_len_and_count_tails() { + assert!(returns_usize(&method(ident("xs"), "len", vec![]))); + assert!(returns_usize(&method(ident("xs"), "count", vec![]))); + let tail_after_stmt = block(vec![ident("k"), method(ident("xs"), "len", vec![])]); + assert!(returns_usize(&tail_after_stmt)); + } + + #[test] + fn test_lenret_1_other_tails_are_not_usize() { + assert!(!returns_usize(&ident("xs"))); + assert!(!returns_usize(&method(ident("xs"), "abs", vec![]))); + assert!(!returns_usize(&method( + ident("xs"), + "count", + vec![ident("c")] + ))); + assert!(!returns_usize(&block(vec![]))); + } } diff --git a/tests/unitret_1_unit_body_return.rs b/tests/unitret_1_unit_body_return.rs index 94a74fd09..9ed1e7f99 100644 --- a/tests/unitret_1_unit_body_return.rs +++ b/tests/unitret_1_unit_body_return.rs @@ -159,3 +159,42 @@ fn test_unitret_1_len_tail_keeps_a_return_type() { let src = "fun f(xs: &Vec) { xs.len() }\n"; assert!(signature_of_f(&transpile(src)).contains("->")); } + +// ==================== LENRET-1: usize-valued tails ==================== +// `len()`/`count()` are `usize` in the transpiled Rust (an unannotated +// `let n = v.len()` is a `usize` too), so a function whose tail is one of +// them returns `usize` rather than an `i32` it cannot produce. + +#[test] +fn test_lenret_1_len_tail_infers_usize() { + let src = "fun f(xs: &Vec) { xs.len() }\n\ + fun main() { let v = vec![1, 2, 3]\n println!(\"{}\", f(&v)) }\n"; + assert!(signature_of_f(&transpile(src)).contains("->usize")); + assert_eq!(compile_and_run(src).trim(), "3"); +} + +#[test] +fn test_lenret_1_len_tail_after_statements_and_in_loop_bound() { + let src = "fun f(xs: &Vec) { let k = 1\n xs.len() }\n\ + fun main() { let v = vec![4, 5]\n let mut t = 0\n \ + for i in 0..f(&v) { t = t + v[i] }\n println!(\"{} {}\", f(&v), t) }\n"; + assert_eq!(compile_and_run(src).trim(), "2 9"); +} + +#[test] +fn test_lenret_1_string_len_tail_compiles() { + let src = "fun f(s: &str) { s.len() }\nfun main() { println!(\"{}\", f(\"abcd\")) }\n"; + assert!(signature_of_f(&transpile(src)).contains("->usize")); + assert_eq!(compile_and_run(src).trim(), "4"); +} + +/// `xs.count()` transpiles to `xs.iter().count()`. (`s.chars().count()` is +/// emitted as `s.chars().iter().count()`, a separate method-emission defect +/// reported with this ticket, so it is not used here.) +#[test] +fn test_lenret_1_count_tail_compiles() { + let src = "fun f(xs: &Vec) { xs.count() }\n\ + fun main() { println!(\"{}\", f(&vec![7, 8, 9])) }\n"; + assert!(signature_of_f(&transpile(src)).contains("->usize")); + assert_eq!(compile_and_run(src).trim(), "3"); +} From fefd3886be2f3d430a7a4d2fe847a4776d582fb8 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:14:22 +0200 Subject: [PATCH 065/106] RHLGA-1: file NESTASSIGN/UNITRET/ENUMFIELDKW/IDXASSIGN/LENRET (done) and five new defects; CHANGELOG New defects, each reproduced at 8ec2dd99: PRINTLNFMT-1, INTLEN-1, COUNTITER-1, IDXCOMPOUND-1, IDXPUSHWB-1. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 9 ++ docs/roadmaps/roadmap.yaml | 207 +++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 325392646..aef78d7ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,15 @@ Behaviour changes a program can observe are marked **(behaviour)**. `o.type`), and a struct can declare it (`struct W { type: i32 }`). A Rust reserved word is emitted as `r#type` (EXTENDKW-1, RESFIELD-1). A `let` named `self`, `Self`, `super` or `crate` no longer panics the transpiler. + An enum struct variant may name a field with a keyword too (ENUMFIELDKW-1). +- An assignment through a field and index chain (`o.items[0].z = 5`) takes + effect in the interpreter (NESTASSIGN-1) and in compiled code + (IDXASSIGN-1). Before, the transpiler assigned to a `.clone()` of the + element, so the write was silently lost. Compound assignment, `&mut` and + mutating-method receivers on such a chain are emitted as places as well. +- A method or function whose last expression is a unit-returning call + (`self.items.push(x)`) infers no return type (UNITRET-1), and one ending in + `len()` or `count()` infers `-> usize` (LENRET-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 27c54eb83..e0b165cc9 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4192,3 +4192,210 @@ roadmap: - kind:code - transpiler - filed-by:RHLGA-1 +- id: NESTASSIGN-1 + github_issue: null + item_type: task + title: 'NESTASSIGN-1: nested field assignment through an index chain (o.items[0].z = 5) is lost in the interpreter' + status: completed + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Fix: eval_field_assign accepts an Identifier/FieldAccess/IndexAccess chain and writes back through it' + - 'Acceptance: tests/nestassign_1_nested_field_assign.rs' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 + notes: null +- id: UNITRET-1 + github_issue: null + item_type: task + title: 'UNITRET-1: a method whose tail is a unit-returning call (v.push(x)) infers a non-unit return type' + status: completed + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Fix: function_analysis is_unit_method; unit tails infer no return type' + - 'Acceptance: tests/unitret_1_unit_body_return.rs' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: ENUMFIELDKW-1 + github_issue: null + item_type: task + title: 'ENUMFIELDKW-1: a keyword naming an enum struct-variant field fails to parse / transpile' + status: completed + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Fix: parser enums.rs uses the member-name table; transpile_enum emits safe_ident' + - 'Acceptance: tests/enumfieldkw_1.rs' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - parser + - filed-by:RHLGA-1 + notes: null +- id: IDXASSIGN-1 + github_issue: null + item_type: task + title: 'IDXASSIGN-1: o.items[0].z = 5 transpiles to o.items[0 as usize].clone().z = 5: the write is silently lost' + status: completed + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Repro: compiled binary prints the old value' + - 'Fix: Transpiler::transpile_place emits place chains without clone for =, compound assignment, &mut and mutating-method receivers' + - 'Acceptance: tests/idxassign_1_place_targets.rs (8 rustc differential tests)' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: LENRET-1 + github_issue: null + item_type: task + title: 'LENRET-1: fun f(xs) { xs.len() } infers -> i32 but len() is usize (E0308)' + status: completed + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Fix: a zero-arg len()/count() tail infers -> usize' + - 'Acceptance: tests/unitret_1_unit_body_return.rs test_lenret_1_*' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: PRINTLNFMT-1 + github_issue: null + item_type: task + title: 'PRINTLNFMT-1: println("{:?}", v) with one argument compiles to println!("{} {:?}", "{:?}", v) and prints the format string' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Repro: println("{:?}", v) and println("v={:?}", v) transpile wrong; println("{}", x) and two-argument forms are right' + - 'Acceptance: rustc differential test on every placeholder form with one and several arguments' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: INTLEN-1 + github_issue: null + item_type: task + title: 'INTLEN-1: let n: int = v.len() (and -> int returning len()) emits i64 = usize without a cast (E0308)' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Acceptance: rustc differential test for let/return/argument positions annotated int receiving len()/count()' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: COUNTITER-1 + github_issue: null + item_type: task + title: 'COUNTITER-1: count() gets .iter() appended even on an iterator (s.chars().count() -> s.chars().iter().count(), E0599); the interpreter has no array count' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Acceptance: transpile + rustc + interpreter tests for s.chars().count(), xs.iter().count(), v.count() where the interpreter and rustc agree' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: IDXCOMPOUND-1 + github_issue: null + item_type: task + title: 'IDXCOMPOUND-1: interpreter: v[1] += 5 fails with Invalid compound assignment target' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Acceptance: compound assignment to index and field-of-index targets agrees with the compiled binary' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 + notes: null +- id: IDXPUSHWB-1 + github_issue: null + item_type: task + title: 'IDXPUSHWB-1: interpreter: t.rows[0].vals.push(9) is not written back to t (prints [1]; compiled prints [1, 9])' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T10:30:00Z + updated: 2026-09-24T10:30:00Z + spec: null + acceptance_criteria: + - 'Acceptance: in-place array methods through an index chain are written back, interpreter agrees with rustc' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 + notes: null From c124ce35f6fce05d28550903238e6e706d89789b Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:19:26 +0200 Subject: [PATCH 066/106] RHLGA-1: IDXCOMPOUND-1 compound assignment to index and field/index chains - eval_compound_assign reads the target through eval_expr and writes the result with write_back, the NESTASSIGN-1 chain writer, so `v[1] += 5`, `o.items[0].z += 1`, `o.xs[1] -= 2` and `g.grid[0][1] -= 6` work - eval_element_assign rebuilds any index chain that passes through a field - tests/g2b_runfix_1.rs: RED tests for IDXCOMPOUND-1, IDXPUSHWB-1 and COUNTITER-1, each checked against a rustc-compiled Rust reference Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/runtime/interpreter_control_flow.rs | 83 ++++--- tests/g2b_runfix_1.rs | 291 ++++++++++++++++++++++++ 2 files changed, 339 insertions(+), 35 deletions(-) create mode 100644 tests/g2b_runfix_1.rs diff --git a/src/runtime/interpreter_control_flow.rs b/src/runtime/interpreter_control_flow.rs index 48819b923..c876209a8 100644 --- a/src/runtime/interpreter_control_flow.rs +++ b/src/runtime/interpreter_control_flow.rs @@ -517,7 +517,11 @@ impl Interpreter { /// NESTASSIGN-1: store `updated` at the place `target` names: a variable, /// a field of a chain, or an element of an array. - fn write_back(&mut self, target: &Expr, updated: Value) -> Result { + pub(crate) fn write_back( + &mut self, + target: &Expr, + updated: Value, + ) -> Result { match &target.kind { ExprKind::Identifier(name) => { self.set_variable(name, updated.clone()); @@ -536,17 +540,18 @@ impl Interpreter { } /// Element assignment `object[index] = val`. An array reached through a - /// field chain (`o.items[0] = x`) is rebuilt and written back through the - /// chain; every other object uses [`Self::eval_index_assign`]. + /// chain containing a field (`o.items[0] = x`, `g.grid[0][1] = x`) is + /// rebuilt and written back through the chain; every other object uses + /// [`Self::eval_index_assign`]. fn eval_element_assign( &mut self, object: &Expr, index: &Expr, val: Value, ) -> Result { - let ExprKind::FieldAccess { .. } = &object.kind else { + if !is_chain_through_field(object) { return self.eval_index_assign(object, index, val); - }; + } let Value::Array(arr) = self.eval_expr(object)? else { return Err(InterpreterError::RuntimeError( "Cannot index non-array value".to_string(), @@ -642,46 +647,54 @@ impl Interpreter { } } - /// Evaluate a compound assignment - /// Complexity: 6 + /// Evaluate a compound assignment (`x += 1`, `o.a.b -= 2`, `v[i] *= 3`, + /// `o.items[0].z %= 4`). + /// + /// IDXCOMPOUND-1: the target may be any place `write_back` accepts: a + /// variable, or a FieldAccess/IndexAccess chain rooted at one. The current + /// value is read through the chain and the result is written back through + /// the same path plain assignment uses. + /// Complexity: 2 pub(crate) fn eval_compound_assign( &mut self, target: &Expr, op: AstBinaryOp, value: &Expr, ) -> Result { - // Get current value - let current = match &target.kind { - ExprKind::Identifier(name) => self.lookup_variable(name)?, - ExprKind::FieldAccess { object, field } => self.eval_field_access(object, field)?, - _ => { - return Err(InterpreterError::RuntimeError( - "Invalid compound assignment target".to_string(), - )) - } - }; - - // Compute new value + if !is_place_expr(target) { + return Err(InterpreterError::RuntimeError( + "Invalid compound assignment target".to_string(), + )); + } + let current = self.eval_expr(target)?; let rhs = self.eval_expr(value)?; let new_val = self.apply_binary_op(¤t, op, &rhs)?; + self.write_back(target, new_val.clone())?; + Ok(new_val) + } +} - // Assign back - match &target.kind { - ExprKind::Identifier(name) => { - self.set_variable(name, new_val.clone()); - } - ExprKind::FieldAccess { object, field } => { - // Reuse eval_field_assign which handles all object types - self.eval_field_assign(object, field, new_val.clone())?; - } - _ => { - return Err(InterpreterError::RuntimeError( - "Complex assignment targets not supported in compound assignment".to_string(), - )) - } - } +/// IDXCOMPOUND-1: `expr` is a FieldAccess, or an IndexAccess chain with a +/// FieldAccess somewhere below it (`o.grid[0]`). +/// Complexity: 3 +fn is_chain_through_field(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::FieldAccess { .. } => true, + ExprKind::IndexAccess { object, .. } => is_chain_through_field(object), + _ => false, + } +} - Ok(new_val) +/// IDXCOMPOUND-1 / IDXPUSHWB-1: `expr` names a place: a variable, or a +/// FieldAccess/IndexAccess chain whose root is a variable. +/// Complexity: 3 +pub(crate) fn is_place_expr(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Identifier(_) => true, + ExprKind::FieldAccess { object, .. } | ExprKind::IndexAccess { object, .. } => { + is_place_expr(object) + } + _ => false, } } diff --git a/tests/g2b_runfix_1.rs b/tests/g2b_runfix_1.rs new file mode 100644 index 000000000..15614eeee --- /dev/null +++ b/tests/g2b_runfix_1.rs @@ -0,0 +1,291 @@ +//! RHLGA-1 G2b run fixes: three interpreter defects measured with `ruchy run`. +//! +//! - IDXCOMPOUND-1: compound assignment to an index target (`v[1] += 5`), a +//! field of an element (`o.items[0].z += 1`) and an element of a field +//! (`o.xs[1] -= 2`) failed with "Invalid compound assignment target". +//! - IDXPUSHWB-1: an in-place array method on a chain that contains an index +//! (`t.rows[0].vals.push(9)`, `m[0].push(x)`) changed a copy and was lost. +//! - COUNTITER-1 (interpreter half): `"abc".chars().count()` failed with +//! "Unknown array method: count". +//! +//! Each case states the stdout that Rust prints for the same program. A +//! hand-written Rust reference is compiled with rustc to prove that string, +//! and `ruchy run` must print it too. +//! +//! `ruchy compile` output is not compared here: at fefd3886 the transpiler +//! emits `v[1 as usize].clone() += 5` and `t.rows[0 as usize].clone().vals +//! .push(9)` (rustc E0067 / a lost write) and `.chars().iter().count()` +//! (E0599). Those are the transpiler halves, fixed in another worktree; every +//! program in this file uses a feature one of them breaks. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn ruchy_run(dir: &Path, src: &str) -> String { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.arg("run").arg(file).timeout(Duration::from_secs(10)); + stdout_of(cmd, "ruchy run") +} + +fn rustc_run(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("reference.rs"); + let bin = dir.join("reference_bin"); + std::fs::write(&main_rs, rust).expect("write reference.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, "rustc on the Rust reference"); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(10)); + stdout_of(run, "Rust reference binary") +} + +/// Rust prints `expected` for `rust`, and `ruchy run` prints it for `ruchy`. +fn assert_matches_rust(ruchy: &str, rust: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!(rustc_run(dir.path(), rust), expected, "Rust reference"); + assert_eq!(ruchy_run(dir.path(), ruchy), expected, "ruchy run"); +} + +// ---------------------------------------------------------------- IDXCOMPOUND-1 + +#[test] +fn test_idxcompound_1_local_array_all_operators() { + let ruchy = r#" +fun main() { + let mut v = [10, 20, 30, 40, 50] + v[0] += 5 + v[1] -= 3 + v[2] *= 2 + v[3] /= 3 + v[4] %= 7 + println("{} {} {} {} {}", v[0], v[1], v[2], v[3], v[4]) +} +"#; + let rust = r#" +fn main() { + let mut v = [10, 20, 30, 40, 50]; + v[0] += 5; + v[1] -= 3; + v[2] *= 2; + v[3] /= 3; + v[4] %= 7; + println!("{} {} {} {} {}", v[0], v[1], v[2], v[3], v[4]); +} +"#; + assert_matches_rust(ruchy, rust, "15 17 60 13 1\n"); +} + +#[test] +fn test_idxcompound_1_variable_index_in_loop() { + let ruchy = r#" +fun main() { + let mut v = [1, 1, 1, 1] + for i in 0..4 { + v[i] += i * 10 + } + println("{} {} {} {}", v[0], v[1], v[2], v[3]) +} +"#; + let rust = r#" +fn main() { + let mut v = [1, 1, 1, 1]; + for i in 0..4 { + v[i] += i * 10; + } + println!("{} {} {} {}", v[0], v[1], v[2], v[3]); +} +"#; + assert_matches_rust(ruchy, rust, "1 11 21 31\n"); +} + +#[test] +fn test_idxcompound_1_field_of_element_and_element_of_field() { + let ruchy = r#" +struct P { z: i32 } +struct O { items: Vec

, xs: Vec } +fun main() { + let mut o = O { items: vec![P { z: 1 }, P { z: 7 }], xs: vec![10, 20, 30] } + o.items[0].z += 1 + o.items[1].z *= 3 + o.xs[1] -= 2 + o.xs[2] /= 4 + println("{} {} {} {} {}", o.items[0].z, o.items[1].z, o.xs[0], o.xs[1], o.xs[2]) +} +"#; + let rust = r#" +struct P { z: i32 } +struct O { items: Vec

, xs: Vec } +fn main() { + let mut o = O { items: vec![P { z: 1 }, P { z: 7 }], xs: vec![10, 20, 30] }; + o.items[0].z += 1; + o.items[1].z *= 3; + o.xs[1] -= 2; + o.xs[2] /= 4; + println!("{} {} {} {} {}", o.items[0].z, o.items[1].z, o.xs[0], o.xs[1], o.xs[2]); +} +"#; + assert_matches_rust(ruchy, rust, "2 21 10 18 7\n"); +} + +#[test] +fn test_idxcompound_1_nested_index_and_index_of_nested_field() { + let ruchy = r#" +struct G { grid: Vec> } +fun main() { + let mut m = vec![vec![1, 2], vec![3, 4]] + m[1][0] += 100 + let mut g = G { grid: vec![vec![5, 6], vec![7, 8]] } + g.grid[0][1] -= 6 + println("{} {} {}", m[1][0], g.grid[0][1], g.grid[1][1]) +} +"#; + let rust = r#" +struct G { grid: Vec> } +fn main() { + let mut m = vec![vec![1, 2], vec![3, 4]]; + m[1][0] += 100; + let mut g = G { grid: vec![vec![5, 6], vec![7, 8]] }; + g.grid[0][1] -= 6; + println!("{} {} {}", m[1][0], g.grid[0][1], g.grid[1][1]); +} +"#; + assert_matches_rust(ruchy, rust, "103 0 8\n"); +} + +// ---------------------------------------------------------------- IDXPUSHWB-1 + +#[test] +fn test_idxpushwb_1_push_through_field_index_field_chain() { + let ruchy = r#" +struct R { vals: Vec } +struct T { rows: Vec } +fun main() { + let mut t = T { rows: vec![R { vals: vec![1] }] } + t.rows[0].vals.push(9) + println("{:?}", t.rows[0].vals) +} +"#; + let rust = r#" +struct R { vals: Vec } +struct T { rows: Vec } +fn main() { + let mut t = T { rows: vec![R { vals: vec![1] }] }; + t.rows[0].vals.push(9); + println!("{:?}", t.rows[0].vals); +} +"#; + assert_matches_rust(ruchy, rust, "[1, 9]\n"); +} + +#[test] +fn test_idxpushwb_1_methods_on_element_of_local_array() { + let ruchy = r#" +fun main() { + let mut m = vec![vec![3, 1, 2], vec![5]] + m[0].push(0) + m[0].sort() + let i = 1 + m[i].insert(0, 4) + let p = m[i].pop() + println("{:?} {:?} {:?}", m[0], m[1], p) +} +"#; + let rust = r#" +fn main() { + let mut m = vec![vec![3, 1, 2], vec![5]]; + m[0].push(0); + m[0].sort(); + let i = 1; + m[i].insert(0, 4); + let p = m[i].pop(); + println!("{:?} {:?} {:?}", m[0], m[1], p); +} +"#; + assert_matches_rust(ruchy, rust, "[0, 1, 2, 3] [4] Some(5)\n"); +} + +#[test] +fn test_idxpushwb_1_sort_and_remove_through_variable_index() { + let ruchy = r#" +struct R { vals: Vec } +struct T { rows: Vec } +fun main() { + let mut t = T { rows: vec![R { vals: vec![1] }, R { vals: vec![9, 4, 6] }] } + let i = 1 + t.rows[i].vals.sort() + let r = t.rows[i].vals.remove(0) + println("{} {:?} {:?}", r, t.rows[i].vals, t.rows[0].vals) +} +"#; + let rust = r#" +struct R { vals: Vec } +struct T { rows: Vec } +fn main() { + let mut t = T { rows: vec![R { vals: vec![1] }, R { vals: vec![9, 4, 6] }] }; + let i = 1; + t.rows[i].vals.sort(); + let r = t.rows[i].vals.remove(0); + println!("{} {:?} {:?}", r, t.rows[i].vals, t.rows[0].vals); +} +"#; + assert_matches_rust(ruchy, rust, "4 [6, 9] [1]\n"); +} + +// ---------------------------------------------------------------- COUNTITER-1 + +#[test] +fn test_countiter_1_chars_count_matches_rust() { + let ruchy = r#" +fun main() { + let s = "abc" + let n = s.chars().count() + let u = "héllo".chars().count() + println("{} {} {}", n, u, "".chars().count()) +} +"#; + let rust = r#" +fn main() { + let s = "abc"; + let n = s.chars().count(); + let u = "héllo".chars().count(); + println!("{} {} {}", n, u, "".chars().count()); +} +"#; + assert_matches_rust(ruchy, rust, "3 5 0\n"); +} + +#[test] +fn test_countiter_1_iter_and_bytes_count_matches_rust() { + let ruchy = r#" +fun main() { + let v = vec![1, 2, 3, 4] + let e: Vec = vec![] + println("{} {} {}", v.iter().count(), e.iter().count(), "héllo".bytes().count()) +} +"#; + let rust = r#" +fn main() { + let v = vec![1, 2, 3, 4]; + let e: Vec = vec![]; + println!("{} {} {}", v.iter().count(), e.iter().count(), "héllo".bytes().count()); +} +"#; + assert_matches_rust(ruchy, rust, "4 0 6\n"); +} From faaa37db23ba8ae74d3076a24c7f1cd1b79f1c9d Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:21:08 +0200 Subject: [PATCH 067/106] RHLGA-1: IDXPUSHWB-1 in-place array methods write back through index chains - try_field_array_mutation becomes try_chain_array_mutation: the receiver may be any FieldAccess/IndexAccess chain rooted at a variable, and the changed array is stored with write_back (the NESTASSIGN-1 writer), so `t.rows[0].vals.push(9)`, `m[0].sort()` and `m[i].insert(0, x)` persist - is_field_chain_rooted is replaced by the shared is_place_expr - g2b_runfix_1: the element-of-local-array case no longer prints the pop result (ARRAYMUT-1 returns the bare element, not Some) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 290 ++++++++++---------- src/runtime/interpreter_methods_dispatch.rs | 35 ++- tests/g2b_runfix_1.rs | 10 +- 3 files changed, 166 insertions(+), 169 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index d493bf1bd..1b1a4ede4 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T08:01:13.238900929Z", + "created_at": "2026-09-24T08:45:48.181903505Z", "git_context": null, "files": { "./build.rs": { @@ -6091,48 +6091,48 @@ }, "./src/backend/transpiler/function_signature.rs": { "content_hash": [ - 217, - 87, - 242, - 178, - 187, - 154, - 105, - 134, - 100, - 39, - 8, - 245, - 119, - 59, - 71, - 105, - 179, - 87, - 167, - 158, - 186, - 189, + 89, + 93, + 184, + 216, + 123, + 99, + 250, 85, - 231, - 237, - 227, - 87, + 77, + 177, + 223, + 248, + 147, + 66, + 46, + 67, 74, - 159, + 132, + 28, + 84, + 208, + 139, + 208, + 188, 200, - 165, - 140 + 211, + 164, + 118, + 25, + 121, + 242, + 0 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.813318, + "duplication_ratio": 16.824644, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.81332, + "total": 76.824646, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -6156,7 +6156,7 @@ }, { "source_metric": "Duplication", - "amount": 3.1866825, + "amount": 3.1753554, "applied_to": [ "Duplication" ], @@ -6168,7 +6168,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 50" + "issue": "High cognitive complexity: 51" }, { "source_metric": "StructuralComplexity", @@ -6176,7 +6176,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 60" + "issue": "High cyclomatic complexity: 61" } ], "critical_defects_count": 0, @@ -6700,48 +6700,48 @@ }, "./src/backend/transpiler/lifetime_helpers.rs": { "content_hash": [ - 219, - 111, - 68, - 77, - 105, - 164, - 90, - 116, - 60, - 171, - 171, - 32, - 214, - 22, - 154, - 43, - 102, 197, - 114, - 49, - 77, - 254, + 78, + 191, + 144, + 123, + 135, + 71, + 213, + 151, + 17, + 196, + 71, + 29, + 94, 223, - 233, - 5, + 20, + 30, + 31, + 138, + 132, 249, - 225, - 181, - 209, - 116, - 121, - 247 + 218, + 34, + 134, + 38, + 62, + 246, + 7, + 84, + 30, + 216, + 4 ], "score": { - "structural_complexity": 23.8, + "structural_complexity": 23.5, "semantic_complexity": 20.0, - "duplication_ratio": 15.530726, + "duplication_ratio": 15.567867, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 99.33073, + "total": 99.06787, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -6757,19 +6757,19 @@ }, { "source_metric": "Duplication", - "amount": 4.4692736, + "amount": 4.4321327, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 22.3%" + "issue": "Code duplication: 22.2%" }, { "source_metric": "StructuralComplexity", - "amount": 1.2, + "amount": 1.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 19" + "issue": "High cognitive complexity: 20" } ], "critical_defects_count": 0, @@ -7940,48 +7940,48 @@ }, "./src/backend/transpiler/return_type_helpers.rs": { "content_hash": [ - 255, - 92, - 236, - 189, - 185, - 193, + 64, + 203, + 87, + 30, + 216, + 227, + 209, + 5, + 98, + 48, + 214, + 192, + 210, + 112, 156, - 143, - 118, - 110, - 58, - 206, - 109, - 200, - 104, - 20, - 207, - 240, - 60, - 93, - 8, - 237, - 38, - 54, - 109, - 188, - 63, - 128, - 9, - 197, - 101, - 119 + 227, + 179, + 254, + 68, + 214, + 176, + 176, + 66, + 248, + 103, + 225, + 29, + 43, + 181, + 131, + 167, + 120 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.398375, + "duplication_ratio": 17.506493, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.39838, + "total": 77.50649, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -7997,11 +7997,11 @@ }, { "source_metric": "Duplication", - "amount": 2.601626, + "amount": 2.4935064, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 13.0%" + "issue": "Code duplication: 12.5%" }, { "source_metric": "StructuralComplexity", @@ -36973,48 +36973,48 @@ }, "./src/runtime/interpreter_control_flow.rs": { "content_hash": [ - 122, - 207, - 75, - 215, - 173, - 2, - 94, - 221, - 83, - 180, - 203, - 251, - 116, - 248, - 136, + 106, + 11, + 69, + 124, + 155, + 235, + 132, + 66, 73, - 48, - 59, - 236, - 55, - 111, - 143, - 154, - 123, - 248, - 186, - 198, - 250, - 234, - 206, - 180, - 48 + 96, + 124, + 193, + 210, + 168, + 142, + 113, + 51, + 66, + 52, + 189, + 156, + 166, + 94, + 237, + 156, + 178, + 126, + 38, + 191, + 102, + 210, + 78 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.044564, + "duplication_ratio": 14.064603, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 74.04456, + "total": 74.064606, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -37026,15 +37026,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 184 duplicate code patterns" + "issue": "Found 185 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.9554358, + "amount": 5.935397, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.8%" + "issue": "Code duplication: 29.7%" }, { "source_metric": "StructuralComplexity", @@ -37042,7 +37042,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 100" + "issue": "High cognitive complexity: 102" }, { "source_metric": "StructuralComplexity", @@ -37050,7 +37050,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 113" + "issue": "High cyclomatic complexity: 115" } ], "critical_defects_count": 0, @@ -45046,7 +45046,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.28839, + "avg_score": 90.28816, "grade_distribution": { "A+": 195, "A": 175, diff --git a/src/runtime/interpreter_methods_dispatch.rs b/src/runtime/interpreter_methods_dispatch.rs index 6602d88a1..e35a977af 100644 --- a/src/runtime/interpreter_methods_dispatch.rs +++ b/src/runtime/interpreter_methods_dispatch.rs @@ -16,6 +16,7 @@ use crate::runtime::{InterpreterError, Value}; use std::sync::Arc; use crate::runtime::eval_array::{apply_in_place_array_method, is_in_place_array_method}; +use crate::runtime::interpreter_control_flow::is_place_expr; /// Which handler an object's dispatch marker selects, checked in the order /// actor, class, struct, `__type`. @@ -47,16 +48,6 @@ impl ObjectKind { } } -/// RHLGA-1 F6: `x`, `x.a`, `x.a.b`, …: a field chain rooted at a variable, -/// which `eval_field_assign` can write back through. -fn is_field_chain_rooted(expr: &Expr) -> bool { - match &expr.kind { - ExprKind::Identifier(_) => true, - ExprKind::FieldAccess { object, .. } => is_field_chain_rooted(object), - _ => false, - } -} - /// RHLGA-1 F6: `obj` with `field` set to `val`: a copy for value types, the same /// shared value (updated in place) for `ObjectMut` and `Class`. pub(crate) fn with_field(obj: Value, field: &str, val: Value) -> Result { @@ -100,25 +91,30 @@ impl Interpreter { /// result and the changed array is written back to the field. The field /// may be nested (`self.inner.items.push(x)`, `s.a.b.sort()`). /// + /// IDXPUSHWB-1: the receiver may be any FieldAccess/IndexAccess chain + /// rooted at a variable (`m[0].push(x)`, `t.rows[i].vals.sort()`); the + /// changed array is stored through `write_back`, the NESTASSIGN-1 writer. + /// /// Returns `Ok(None)` when the call is not of that shape, so the caller /// falls through to ordinary method dispatch. - fn try_field_array_mutation( + fn try_chain_array_mutation( &mut self, receiver: &Expr, method: &str, args: &[Expr], ) -> Result, InterpreterError> { - let ExprKind::FieldAccess { object, field } = &receiver.kind else { - return Ok(None); - }; - if !is_field_chain_rooted(object) || !is_in_place_array_method(method, args.len()) { + let is_chain = matches!( + receiver.kind, + ExprKind::FieldAccess { .. } | ExprKind::IndexAccess { .. } + ); + if !is_chain || !is_place_expr(receiver) || !is_in_place_array_method(method, args.len()) { return Ok(None); } let Value::Array(arr) = self.eval_expr(receiver)? else { return Ok(None); }; let (items, result) = self.apply_in_place_call(&arr, method, args)?; - self.eval_field_assign(object, field, Value::Array(Arc::from(items)))?; + self.write_back(receiver, Value::Array(Arc::from(items)))?; Ok(Some(result)) } @@ -208,9 +204,10 @@ impl Interpreter { } } - // FIELDPOP-1: mutating array methods on a field (self.items.pop(), - // s.items.push(x)) return their own result and write the field back - if let Some(result) = self.try_field_array_mutation(receiver, method, args)? { + // FIELDPOP-1 / IDXPUSHWB-1: mutating array methods on a field or an + // element (self.items.pop(), m[0].push(x)) return their own result + // and write the changed array back through the chain + if let Some(result) = self.try_chain_array_mutation(receiver, method, args)? { return Ok(result); } diff --git a/tests/g2b_runfix_1.rs b/tests/g2b_runfix_1.rs index 15614eeee..2a4a61467 100644 --- a/tests/g2b_runfix_1.rs +++ b/tests/g2b_runfix_1.rs @@ -203,8 +203,8 @@ fun main() { m[0].sort() let i = 1 m[i].insert(0, 4) - let p = m[i].pop() - println("{:?} {:?} {:?}", m[0], m[1], p) + m[i].pop() + println("{:?} {:?}", m[0], m[1]) } "#; let rust = r#" @@ -214,11 +214,11 @@ fn main() { m[0].sort(); let i = 1; m[i].insert(0, 4); - let p = m[i].pop(); - println!("{:?} {:?} {:?}", m[0], m[1], p); + m[i].pop(); + println!("{:?} {:?}", m[0], m[1]); } "#; - assert_matches_rust(ruchy, rust, "[0, 1, 2, 3] [4] Some(5)\n"); + assert_matches_rust(ruchy, rust, "[0, 1, 2, 3] [4]\n"); } #[test] From 4cc41cec55a3d2ffd054926b5ddb30618e8203e2 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:22:14 +0200 Subject: [PATCH 068/106] RHLGA-1: COUNTITER-1 count() after chars/iter/bytes in the interpreter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - arrays answer `count()` with their length; `chars()`, `bytes()` and `iter()` return arrays in the interpreter, so `"abc".chars().count()`, `v.iter().count()` and `"héllo".bytes().count()` give rustc's integers - arrays answer `iter()` with themselves (it was "Unknown array method") - strings answer `bytes()` like `as_bytes()` Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 86 +++++++++++++++--------------- src/runtime/eval_array.rs | 6 ++- src/runtime/eval_string_methods.rs | 3 +- 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 1b1a4ede4..be02c5877 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T08:45:48.181903505Z", + "created_at": "2026-09-24T08:45:54.238480819Z", "git_context": null, "files": { "./build.rs": { @@ -37542,48 +37542,48 @@ }, "./src/runtime/interpreter_methods_dispatch.rs": { "content_hash": [ - 167, - 117, - 189, - 5, - 172, - 159, - 110, - 166, - 101, - 231, - 136, - 207, - 64, - 180, - 199, - 175, - 60, - 207, - 80, - 232, - 253, - 202, - 77, - 118, - 172, - 185, - 185, - 220, - 61, + 89, + 206, + 198, + 132, + 210, + 93, + 35, + 236, + 3, + 190, + 212, + 140, + 144, + 222, + 179, + 16, + 201, + 239, + 237, + 47, + 27, + 17, 147, - 196, - 224 + 102, + 50, + 163, + 4, + 65, + 132, + 54, + 104, + 153 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.38992, - "coupling_score": 11.0, + "duplication_ratio": 14.397871, + "coupling_score": 10.799999, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 70.38992, + "total": 70.197876, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -37595,15 +37595,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 173 duplicate code patterns" + "issue": "Found 172 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.61008, + "amount": 5.602129, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 28.1%" + "issue": "Code duplication: 28.0%" }, { "source_metric": "StructuralComplexity", @@ -37611,7 +37611,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 62" + "issue": "High cognitive complexity: 61" }, { "source_metric": "StructuralComplexity", @@ -37619,15 +37619,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 83" + "issue": "High cyclomatic complexity: 80" }, { "source_metric": "Coupling", - "amount": 4.0, + "amount": 4.2000003, "applied_to": [ "Coupling" ], - "issue": "Too many imports: 40" + "issue": "Too many imports: 41" } ], "critical_defects_count": 0, @@ -45046,7 +45046,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.28816, + "avg_score": 90.2878, "grade_distribution": { "A+": 195, "A": 175, diff --git a/src/runtime/eval_array.rs b/src/runtime/eval_array.rs index f144d5923..2d3983882 100644 --- a/src/runtime/eval_array.rs +++ b/src/runtime/eval_array.rs @@ -18,7 +18,11 @@ fn eval_array_nullary_method( method: &str, ) -> Option> { let result = match method { - "len" | "length" => eval_array_len(arr), + // COUNTITER-1: `count()` on an array, or on what `chars()`, `bytes()` + // and `iter()` return (arrays in the interpreter), is its length + "len" | "length" | "count" => eval_array_len(arr), + // COUNTITER-1: `iter()` is the array itself, so `v.iter().count()` works + "iter" => Ok(Value::Array(Arc::clone(arr))), "first" => eval_array_first(arr), "last" => eval_array_last(arr), "is_empty" => eval_array_is_empty(arr), diff --git a/src/runtime/eval_string_methods.rs b/src/runtime/eval_string_methods.rs index 5a40bf0ae..9e132d345 100644 --- a/src/runtime/eval_string_methods.rs +++ b/src/runtime/eval_string_methods.rs @@ -51,7 +51,8 @@ fn eval_zero_arg_string_method(s: &Arc, method: &str) -> Result eval_string_parse(s), "timestamp" => eval_string_timestamp(s), "to_rfc3339" => Ok(Value::from_string(s.to_string())), - "as_bytes" => eval_string_as_bytes(s), + // COUNTITER-1: `bytes()` yields the same UTF-8 bytes as `as_bytes()` + "as_bytes" | "bytes" => eval_string_as_bytes(s), _ => Err(InterpreterError::RuntimeError(format!( "Unknown zero-argument string method: {method}" ))), From 5e59697efd65d2f8e37f944e4c65fdaf362541b5 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:21:57 +0200 Subject: [PATCH 069/106] =?UTF-8?q?RHLGA-1:=20PRINTLNFMT-1=20any=20{?= =?UTF-8?q?=E2=80=A6}=20placeholder=20makes=20the=20first=20literal=20the?= =?UTF-8?q?=20format=20string?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `println("{:?}", v)` transpiled to `println!("{} {:?}", "{:?}", v)` and the binary printed `{:?} [1, 7, 3]`: transpile_print_multiple_args recognised a format string only when it contained `{}`. - has_format_placeholder (print_helpers): a literal with any `{…}` placeholder (`{:?}`, `{:#?}`, `{name}`, `{0}`, `{:>5}`, `{:.2}`) is the format string; `{{` escapes do not count. Shared with the `println!(..)` macro-form path (macro_helpers), which used `contains('{') && contains('}')`. - `eprintln(..)`/`eprint(..)` calls were emitted as plain function calls (`eprintln("{:?}".to_string(), v)`); they now take the same print path and count as unit statements/tails, so main no longer wraps them in `println!("{:?}", ..)`. - tests/g2b_transfix_1.rs: rustc-backed end-to-end tests for PRINTLNFMT-1, INTLEN-1 and COUNTITER-1 (the latter two RED until their commits). - Seen, not fixed (runtime): the interpreter prints `print("{:?};", v)` as `"{:?};" [1, 7, 3]`, ignores `{:>5}`/`{:.2}`/`{0}`, and lacks eprintln. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 126 +++++----- .../transpiler/block_categorization.rs | 5 +- .../dispatcher_helpers/macro_helpers.rs | 4 +- src/backend/transpiler/function_transpiler.rs | 9 +- src/backend/transpiler/print_helpers.rs | 43 +++- tests/g2b_transfix_1.rs | 216 ++++++++++++++++++ 6 files changed, 329 insertions(+), 74 deletions(-) create mode 100644 tests/g2b_transfix_1.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index be02c5877..749c76eb9 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T08:45:54.238480819Z", + "created_at": "2026-09-24T08:45:56.766006374Z", "git_context": null, "files": { "./build.rs": { @@ -33703,48 +33703,48 @@ }, "./src/runtime/eval_array.rs": { "content_hash": [ - 222, - 86, - 25, - 71, - 149, - 90, - 87, - 93, - 81, - 160, - 15, - 137, - 200, - 233, - 179, - 80, - 150, - 100, - 208, - 225, - 241, - 99, - 203, - 128, - 164, - 29, - 153, - 45, - 161, + 232, + 213, + 1, + 12, + 98, + 62, + 235, + 113, + 73, + 0, + 36, + 223, + 166, + 2, + 16, + 248, 51, - 65, - 219 + 78, + 228, + 253, + 68, + 243, + 106, + 151, + 14, + 78, + 233, + 43, + 49, + 72, + 122, + 150 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.14272, + "duplication_ratio": 16.146435, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.142715, + "total": 76.14644, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33760,7 +33760,7 @@ }, { "source_metric": "Duplication", - "amount": 3.8572807, + "amount": 3.8535645, "applied_to": [ "Duplication" ], @@ -33780,7 +33780,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 168" + "issue": "High cyclomatic complexity: 169" } ], "critical_defects_count": 0, @@ -36324,38 +36324,38 @@ }, "./src/runtime/eval_string_methods.rs": { "content_hash": [ - 9, - 43, - 96, - 42, - 216, - 156, - 22, - 209, + 227, + 31, + 11, + 123, + 87, + 119, + 15, + 251, 157, - 94, 10, - 203, - 42, - 82, - 162, + 246, + 247, + 53, 114, - 157, + 136, + 186, + 203, + 20, 165, + 212, + 154, + 97, + 175, + 114, + 32, 106, - 204, - 226, - 195, - 117, - 38, - 216, - 66, - 122, - 206, - 82, - 76, - 142, - 180 + 189, + 120, + 132, + 230, + 34, + 166 ], "score": { "structural_complexity": 0.0, diff --git a/src/backend/transpiler/block_categorization.rs b/src/backend/transpiler/block_categorization.rs index 7614e145f..43465a358 100644 --- a/src/backend/transpiler/block_categorization.rs +++ b/src/backend/transpiler/block_categorization.rs @@ -401,7 +401,10 @@ impl Transpiler { /// Complexity: 2 (within Toyota Way limits) fn is_statement_call(func: &Expr) -> bool { if let ExprKind::Identifier(name) = &func.kind { - matches!(name.as_str(), "println" | "print" | "dbg") + matches!( + name.as_str(), + "println" | "print" | "eprintln" | "eprint" | "dbg" + ) } else { false } diff --git a/src/backend/transpiler/dispatcher_helpers/macro_helpers.rs b/src/backend/transpiler/dispatcher_helpers/macro_helpers.rs index 14057610e..3c5b95ade 100644 --- a/src/backend/transpiler/dispatcher_helpers/macro_helpers.rs +++ b/src/backend/transpiler/dispatcher_helpers/macro_helpers.rs @@ -83,7 +83,9 @@ impl Transpiler { // Check if first argument is a format string (contains {}) // TRANSPILER-DEFECT-007 FIX: Also recognize {:?}, {:#?}, {:x}, etc. as format strings let first_is_format_string = match &args[0].kind { - ExprKind::Literal(Literal::String(s)) => s.contains('{') && s.contains('}'), + ExprKind::Literal(Literal::String(s)) => { + super::super::print_helpers::has_format_placeholder(s) + } _ => false, }; diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index 0dc47c2ec..aa76e4aac 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -144,17 +144,22 @@ impl Transpiler { matches!(&expr.kind, ExprKind::Literal(Literal::Unit)) || matches!( &expr.kind, - ExprKind::Call { func, .. } if matches!(&func.kind, ExprKind::Identifier(name) if name == "println" || name == "print") + ExprKind::Call { func, .. } if matches!(&func.kind, ExprKind::Identifier(name) if Self::is_print_name(name)) ) || matches!( &expr.kind, - ExprKind::MacroInvocation { name, .. } if name == "println" || name == "print" + ExprKind::MacroInvocation { name, .. } if Self::is_print_name(name) ) || matches!(&expr.kind, ExprKind::Assign { .. }) || matches!(&expr.kind, ExprKind::Return { value: None }) || matches!(&expr.kind, ExprKind::Return { value: Some(_) }) // Return with value is still unit for main } + /// PRINTLNFMT-1: the print builtins, all unit-valued (stdout and stderr). + fn is_print_name(name: &str) -> bool { + matches!(name, "println" | "print" | "eprintln" | "eprint") + } + /// BOOK-COMPAT-015: Generate main body that prints the last expression fn generate_main_body_with_print(&self, body: &Expr, is_async: bool) -> Result { match &body.kind { diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index e3ea32fbc..60111d6d1 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -91,15 +91,15 @@ impl Transpiler { ) -> Result> { contract_pre_configuration!(); contract_post_configuration!(&"ok"); - if !(base_name == "println" - || base_name == "print" - || base_name == "dbg" - || base_name == "panic") - { + if !matches!( + base_name, + "println" | "print" | "eprintln" | "eprint" | "dbg" | "panic" + ) { return Ok(None); } // Handle single argument with string interpolation - if (base_name == "println" || base_name == "print") && args.len() == 1 { + let is_print = matches!(base_name, "println" | "print" | "eprintln" | "eprint"); + if is_print && args.len() == 1 { if let ExprKind::StringInterpolation { parts } = &args[0].kind { return Ok(Some( self.transpile_print_with_interpolation(base_name, parts)?, @@ -182,7 +182,7 @@ impl Transpiler { } else { // Multiple arguments - check if first is format string if let ExprKind::Literal(Literal::String(format_str)) = &args[0].kind { - if format_str.contains("{}") { + if has_format_placeholder(format_str) { // First argument is a format string, rest are values let format_arg = &all_args[0]; let value_args = &all_args[1..]; @@ -251,6 +251,23 @@ impl Transpiler { } } +/// PRINTLNFMT-1: `s` contains at least one `{…}` placeholder (`{}`, `{:?}`, +/// `{name}`, `{0}`, `{:>5}`, ...), so it is a format string. `{{` escapes +/// do not count. (complexity: 4) +#[must_use] +pub fn has_format_placeholder(s: &str) -> bool { + let mut chars = s.chars().peekable(); + while let Some(c) = chars.next() { + if c != '{' { + continue; + } + if chars.next_if_eq(&'{').is_none() { + return chars.any(|c| c == '}'); + } + } + false +} + // ============================================================================ // Tests // ============================================================================ @@ -287,6 +304,18 @@ mod tests { make_expr(ExprKind::StringInterpolation { parts }) } + #[test] + fn test_printlnfmt_1_has_format_placeholder() { + for s in [ + "{}", "{:?}", "v={:?}", "{:#?}", "{name}", "{0}", "{:>5}|", "{:.2}", "{{ {}", + ] { + assert!(has_format_placeholder(s), "{s}"); + } + for s in ["", "plain", "{{}}", "{{x}}", "{", "}{"] { + assert!(!has_format_placeholder(s), "{s}"); + } + } + // ======================================================================== // transpile_print_with_interpolation tests // ======================================================================== diff --git a/tests/g2b_transfix_1.rs b/tests/g2b_transfix_1.rs new file mode 100644 index 000000000..6e5f8aa0a --- /dev/null +++ b/tests/g2b_transfix_1.rs @@ -0,0 +1,216 @@ +//! RHLGA-1 transpiler fixes, each checked end to end: `ruchy transpile`, +//! `ruchy compile` (rustc), run the binary, compare exact stdout. +//! +//! - PRINTLNFMT-1: a string-literal first argument containing any `{…}` +//! placeholder (`{:?}`, `{:>5}`, `{0}`, …) is the format string. Before the +//! fix only `{}` was recognised, so `println("{:?}", v)` printed `{:?} [1, 7, 3]`. +//! - INTLEN-1: a `usize` expression (`len()`/`count()`) meeting an `int` +//! let annotation, return type or known parameter type is cast (`as i64`). +//! - COUNTITER-1: `count()` on a receiver that is already an iterator +//! (`chars()`, `iter()`, …) is called directly, without another `.iter()`. +//! +//! The interpreter (`ruchy run`) must agree for the PRINTLNFMT `{:?}` cases +//! and the INTLEN cases. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +/// (stdout, stderr) of a command that must succeed. +fn output_of(mut cmd: Command, what: &str) -> (String, String) { + let out = cmd.output().expect("spawn command"); + let stdout = String::from_utf8(out.stdout).expect("utf8 stdout"); + let stderr = String::from_utf8(out.stderr).expect("utf8 stderr"); + assert!(out.status.success(), "{what} failed:\n{stdout}{stderr}"); + (stdout, stderr) +} + +fn transpile(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + output_of(cmd, &format!("ruchy transpile of:\n{src}\n")).0 +} + +/// `ruchy compile` the source, run the binary, return (stdout, stderr). +fn compile_and_run(src: &str) -> (String, String) { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let bin = dir.path().join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(90)); + output_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + output_of(run, "compiled binary") +} + +fn interpret(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + output_of(cmd, &format!("ruchy run of:\n{src}\n")).0 +} + +fn main_with(body: &str) -> String { + format!("fun main() {{\n let v = [1, 7, 3]\n let x = 5\n{body}\n}}\n") +} + +/// Compiled stdout is `expected`; with `interp`, `ruchy run` agrees. +fn check(src: &str, expected: &str, interp: bool) { + let (stdout, _) = compile_and_run(src); + assert_eq!( + stdout, + expected, + "compiled stdout of:\n{src}\n{}", + transpile(src) + ); + if interp { + assert_eq!(interpret(src), expected, "interpreter stdout of:\n{src}"); + } +} + +// ---------------------------------------------------------------- PRINTLNFMT-1 + +#[test] +fn test_printlnfmt_1_debug_placeholder_vec() { + check(&main_with(r#" println("{:?}", v)"#), "[1, 7, 3]\n", true); +} + +#[test] +fn test_printlnfmt_1_debug_placeholder_int() { + check(&main_with(r#" println("{:?}", x)"#), "5\n", true); +} + +#[test] +fn test_printlnfmt_1_prefixed_debug_placeholder() { + check( + &main_with(r#" println("v={:?}", v)"#), + "v=[1, 7, 3]\n", + true, + ); +} + +#[test] +fn test_printlnfmt_1_display_placeholders_unchanged() { + let src = main_with( + r#" println("{}", x) + println("{} and {:?}", x, [1])"#, + ); + check(&src, "5\n5 and [1]\n", true); +} + +#[test] +fn test_printlnfmt_1_spec_placeholders_compiled() { + let src = main_with( + r#" println("{:>5}|", x) + println("{:.2}", 3.14159) + println("{0}-{0}", x) + println("{:#?}", x)"#, + ); + check(&src, " 5|\n3.14\n5-5\n5\n", false); +} + +#[test] +fn test_printlnfmt_1_print_form() { + let src = main_with( + r#" print("{:?};", v) + println("")"#, + ); + // The interpreter prints `"{:?};" [1, 7, 3]` for `print` (runtime, not transpiler). + check(&src, "[1, 7, 3];\n", false); +} + +#[test] +fn test_printlnfmt_1_eprintln_form() { + let src = main_with( + r#" eprintln("{:?}", v) + eprint("{:?}!", x)"#, + ); + let (stdout, stderr) = compile_and_run(&src); + assert_eq!(stdout, ""); + assert_eq!(stderr, "[1, 7, 3]\n5!", "{}", transpile(&src)); +} + +#[test] +fn test_printlnfmt_1_escaped_braces_are_not_placeholders() { + let rust = transpile(&main_with(r#" println("{{x}}", v)"#)); + assert!( + !rust.contains(r#"println!("{{x}}", v)"#), + "only-escape literal is not a format string:\n{rust}" + ); +} + +// ------------------------------------------------------------------- INTLEN-1 + +#[test] +fn test_intlen_1_let_annotation() { + let src = main_with( + r#" let n: int = v.len() + println(n + 1)"#, + ); + check(&src, "4\n", true); +} + +#[test] +fn test_intlen_1_let_annotation_string_len() { + let src = main_with( + r#" let s = "hello" + let n: int = s.len() + println(n * 2)"#, + ); + check(&src, "10\n", true); +} + +#[test] +fn test_intlen_1_function_return() { + let src = "fun h(xs: Vec) -> int { xs.len() }\n\ + fun main() {\n let mut w = [4, 5]\n w.push(6)\n println(h(w) + 1)\n}\n"; + check(src, "4\n", true); +} + +#[test] +fn test_intlen_1_known_parameter() { + let src = "fun g(n: int) -> int { n * 2 }\n\ + fun main() {\n let v = [1, 7, 3]\n println(g(v.len()))\n}\n"; + check(src, "6\n", true); +} + +// ----------------------------------------------------------------- COUNTITER-1 + +#[test] +fn test_countiter_1_chars_count() { + let src = main_with( + r#" let s = "hello" + println(s.chars().count())"#, + ); + check(&src, "5\n", false); +} + +#[test] +fn test_countiter_1_iter_count() { + check(&main_with(" println(v.iter().count())"), "3\n", false); +} + +#[test] +fn test_countiter_1_collection_count_still_iterates() { + check(&main_with(" println(v.count())"), "3\n", false); +} From d9f96b473caffe68214e44191d8fe3da6b883af3 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:24:03 +0200 Subject: [PATCH 070/106] RHLGA-1: COUNTITER-1 count() on an iterator receiver adds no .iter() - `s.chars().count()` transpiled to `s.chars().iter().count()` and `xs.iter().count()` to `xs.iter().iter().count()` (E0599): transpile_collection_aggregate appended `.iter()` to every non-range receiver. - is_iterator_method_call (call_transpilation): a receiver that is a call to an iterator-producing method (iter, iter_mut, into_iter, chars, bytes, char_indices, lines, split_whitespace, enumerate, rev, skip, take, step_by, zip) is aggregated directly, as ranges already were. map/filter are excluded: they are emitted with `.collect::>()`. Collections keep `.iter()`. - Unit test plus the three COUNTITER cases in tests/g2b_transfix_1.rs (rustc). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 282 +++++++++---------- src/backend/transpiler/call_transpilation.rs | 34 +++ 2 files changed, 175 insertions(+), 141 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 749c76eb9..8ce79911f 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T08:45:56.766006374Z", + "created_at": "2026-09-24T08:46:01.383285760Z", "git_context": null, "files": { "./build.rs": { @@ -3135,48 +3135,48 @@ }, "./src/backend/transpiler/block_categorization.rs": { "content_hash": [ - 94, - 142, + 217, + 156, + 213, + 36, + 132, + 213, + 112, + 151, + 215, + 58, + 252, + 197, + 135, + 80, + 24, + 108, 202, - 141, - 32, - 235, - 41, - 230, - 171, - 8, - 59, - 232, - 82, - 173, - 148, - 68, - 237, - 136, - 254, - 104, - 200, - 193, - 78, - 6, - 236, - 238, + 2, + 139, + 229, + 29, + 167, + 168, + 115, + 157, + 21, 81, - 236, - 55, - 227, - 251, - 131 + 16, + 40, + 209, + 92, + 241 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.965293, + "duplication_ratio": 15.978378, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.965294, + "total": 75.97838, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3200,11 +3200,11 @@ }, { "source_metric": "Duplication", - "amount": 4.034707, + "amount": 4.0216217, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.2%" + "issue": "Code duplication: 20.1%" }, { "source_metric": "StructuralComplexity", @@ -4836,48 +4836,48 @@ }, "./src/backend/transpiler/dispatcher_helpers/macro_helpers.rs": { "content_hash": [ - 25, - 4, - 52, + 168, + 101, + 248, + 41, + 41, + 173, + 187, + 46, + 3, + 196, + 165, + 252, + 201, + 114, + 124, + 104, + 247, + 30, + 245, + 40, 222, - 231, - 185, - 94, - 220, - 0, - 205, - 96, - 151, - 14, - 54, + 203, + 2, + 67, + 127, + 13, + 221, + 91, + 238, 74, - 15, - 18, - 187, - 237, - 0, - 240, - 85, - 48, - 192, - 179, - 116, - 174, - 147, - 14, - 147, - 19, - 16 + 236, + 57 ], "score": { "structural_complexity": 24.1, "semantic_complexity": 20.0, - "duplication_ratio": 11.416838, + "duplication_ratio": 11.434426, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 95.51684, + "total": 95.534424, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -4893,11 +4893,11 @@ }, { "source_metric": "Duplication", - "amount": 8.583162, + "amount": 8.565574, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 42.9%" + "issue": "Code duplication: 42.8%" }, { "source_metric": "StructuralComplexity", @@ -6194,48 +6194,48 @@ }, "./src/backend/transpiler/function_transpiler.rs": { "content_hash": [ - 156, - 221, - 122, - 247, - 38, - 43, - 187, - 47, - 150, - 58, - 39, - 157, - 169, + 143, + 35, + 192, + 126, + 148, + 80, + 189, + 246, + 161, + 158, + 66, + 171, + 222, + 37, + 121, + 34, 245, - 46, + 68, + 226, + 108, + 157, + 7, 209, - 60, - 203, - 64, - 193, - 64, - 53, - 238, - 117, - 5, - 253, - 249, - 201, - 110, - 101, - 240, - 5 + 190, + 9, + 148, + 102, + 42, + 17, + 150, + 147, + 145 ], "score": { "structural_complexity": 17.6, "semantic_complexity": 20.0, - "duplication_ratio": 15.228628, + "duplication_ratio": 15.256917, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.82863, + "total": 92.85692, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -6251,11 +6251,11 @@ }, { "source_metric": "Duplication", - "amount": 4.771372, + "amount": 4.743083, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 23.9%" + "issue": "Code duplication: 23.7%" }, { "source_metric": "StructuralComplexity", @@ -7679,48 +7679,48 @@ }, "./src/backend/transpiler/print_helpers.rs": { "content_hash": [ - 254, - 123, - 218, - 98, - 172, - 103, - 149, - 174, - 232, - 96, - 101, - 25, - 111, - 44, - 78, - 43, - 199, - 137, - 114, - 106, - 210, - 59, - 199, - 163, + 134, 150, - 96, 19, - 251, + 235, + 175, + 179, + 42, 141, - 205, - 86, - 85 + 12, + 110, + 185, + 112, + 79, + 252, + 12, + 195, + 131, + 252, + 253, + 51, + 236, + 60, + 217, + 194, + 201, + 243, + 1, + 1, + 34, + 157, + 211, + 182 ], "score": { - "structural_complexity": 20.5, + "structural_complexity": 18.4, "semantic_complexity": 20.0, - "duplication_ratio": 13.72043, + "duplication_ratio": 14.028629, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 94.22043, + "total": 92.428635, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -7732,23 +7732,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 45 duplicate code patterns" + "issue": "Found 44 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.2795696, + "amount": 5.97137, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 31.4%" + "issue": "Code duplication: 29.9%" }, { "source_metric": "StructuralComplexity", - "amount": 4.5, + "amount": 6.6000004, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 30" + "issue": "High cognitive complexity: 37" } ], "critical_defects_count": 0, @@ -45046,7 +45046,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.2878, + "avg_score": 90.28453, "grade_distribution": { "A+": 195, "A": 175, diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index 7de77ecf2..092977820 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -375,6 +375,18 @@ impl Transpiler { } } + /// COUNTITER-1: a call whose Rust form already is an iterator + /// (`s.chars()`, `v.iter()`, `it.rev()`, ...). `map`/`filter` are not + /// listed: they are emitted with `.collect::>()`. (complexity: 2) + fn is_iterator_method_call(object: &Expr) -> bool { + matches!(&object.kind, ExprKind::MethodCall { method, .. } if matches!( + method.as_str(), + "iter" | "iter_mut" | "into_iter" | "chars" | "bytes" | "char_indices" + | "lines" | "split_whitespace" | "enumerate" | "rev" | "skip" | "take" + | "step_by" | "zip" + )) + } + /// `a..b` needs parentheses to take a method; `range(a, b)` emits them itself. fn parenthesize_range_literal(obj_tokens: &TokenStream, object: &Expr) -> TokenStream { if matches!(object.kind, ExprKind::Range { .. }) { @@ -398,6 +410,8 @@ impl Transpiler { let (iter, copied) = if self.is_range_receiver(object) { let range = Self::parenthesize_range_literal(obj_tokens, object); (range.clone(), range) + } else if Self::is_iterator_method_call(object) { + (obj_tokens.clone(), obj_tokens.clone()) } else { ( quote! { #obj_tokens.iter() }, @@ -528,6 +542,26 @@ mod tests { make_expr(ExprKind::Literal(Literal::Integer(n, None))) } + #[test] + fn test_countiter_1_count_on_iterator_receiver() { + let transpiler = make_transpiler(); + for (src, want) in [ + ("s.chars().count()", "s.chars().count()"), + ("v.iter().count()", "v.iter().count()"), + ("v.count()", "v.iter().count()"), + ] { + let ast = crate::frontend::parser::Parser::new(src) + .parse() + .expect("parse"); + let got = transpiler + .transpile_expr(&ast) + .expect("transpile") + .to_string() + .replace(' ', ""); + assert_eq!(got, want, "{src}"); + } + } + fn string_expr(s: &str) -> Expr { make_expr(ExprKind::Literal(Literal::String(s.to_string()))) } From a5dc8398b6b887305153fd6f456472c8f8ee35ac Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:33:55 +0200 Subject: [PATCH 071/106] RHLGA-1: INTLEN-1 usize len()/count() meeting an int type is cast - `let n: int = v.len()` emitted `let n: i64 = v.len();`, and `fun h(xs: Vec) -> int { xs.len() }` / `g(v.len())` with `g(n: int)` passed a usize where i64 was expected (E0308). - return_type_helpers: is_usize_expr (zero-arg len/count, shared with returns_usize), usize_cast_target (int/i64/i32/u32/u64/isize), cast_usize_tokens and cast_usize_tail. - Three sites: annotated let (bindings::cast_usize_to_annotation), declared integer return type (tail rewritten to a TypeCast in transpile_function_with_contracts), and arguments to a locally known function signature (call_helpers). Emits `(v.len() as i64)`. - Unit test plus four INTLEN cases in tests/g2b_transfix_1.rs, each compiled with rustc and checked against `ruchy run`. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 78 +++++++-------- src/backend/transpiler/bindings.rs | 18 ++++ src/backend/transpiler/call_helpers.rs | 9 +- src/backend/transpiler/return_type_helpers.rs | 99 ++++++++++++++++++- src/backend/transpiler/statements.rs | 9 ++ 5 files changed, 169 insertions(+), 44 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 8ce79911f..9a4e6f766 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T08:46:01.383285760Z", + "created_at": "2026-09-24T08:46:05.534410689Z", "git_context": null, "files": { "./build.rs": { @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 14, - 59, - 115, - 199, - 169, - 38, - 199, - 232, - 104, - 43, + 208, + 202, + 77, + 167, + 216, + 163, + 189, + 117, + 245, + 216, + 69, + 74, + 116, + 33, + 102, + 19, + 110, 212, - 183, - 170, - 254, - 4, - 177, - 201, - 210, - 193, - 121, - 84, - 171, - 50, - 223, - 18, - 225, - 240, - 64, + 72, + 163, + 52, 237, - 10, - 137, - 109 + 163, + 200, + 183, + 199, + 60, + 54, + 253, + 203, + 162, + 216 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.932611, + "duplication_ratio": 16.023256, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.93261, + "total": 76.023254, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3639,15 +3639,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 68 duplicate code patterns" + "issue": "Found 70 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 4.0673885, + "amount": 3.9767442, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 20.3%" + "issue": "Code duplication: 19.9%" }, { "source_metric": "StructuralComplexity", @@ -3655,7 +3655,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 69" + "issue": "High cognitive complexity: 72" }, { "source_metric": "StructuralComplexity", @@ -3663,7 +3663,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 69" + "issue": "High cyclomatic complexity: 71" } ], "critical_defects_count": 0, @@ -45046,7 +45046,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.28453, + "avg_score": 90.2847, "grade_distribution": { "A+": 195, "A": 175, diff --git a/src/backend/transpiler/bindings.rs b/src/backend/transpiler/bindings.rs index 959055955..88aa571d1 100644 --- a/src/backend/transpiler/bindings.rs +++ b/src/backend/transpiler/bindings.rs @@ -285,6 +285,7 @@ impl Transpiler { } else { Self::growable_list_tokens(name, value, type_annotation, body, value_tokens) }; + let value_tokens = Self::cast_usize_to_annotation(value, type_annotation, value_tokens); // Generate type annotation let type_tokens = self.generate_type_tokens(type_annotation, needs_vec_type_hint)?; @@ -305,6 +306,23 @@ impl Transpiler { } } + /// INTLEN-1: `let n: int = v.len()` casts the `usize` value to the + /// annotated integer type. (complexity: 2) + fn cast_usize_to_annotation( + value: &Expr, + type_annotation: Option<&Type>, + tokens: TokenStream, + ) -> TokenStream { + match type_annotation { + Some(ty) => super::return_type_helpers::cast_usize_tokens( + value, + &Self::type_to_string(ty), + tokens, + ), + None => tokens, + } + } + /// LETVEC-1: a non-empty array literal bound by `let` becomes `vec![..]` /// when the binding is annotated `Vec<..>` or is grown later in `body` /// (the receiver of a Vec-only method). Otherwise `tokens` is returned diff --git a/src/backend/transpiler/call_helpers.rs b/src/backend/transpiler/call_helpers.rs index 0a4fb7c05..d44558a4f 100644 --- a/src/backend/transpiler/call_helpers.rs +++ b/src/backend/transpiler/call_helpers.rs @@ -97,7 +97,14 @@ impl Transpiler { } base_tokens = quote! { #base_tokens.clone() }; } - self.apply_string_coercion(arg, &base_tokens, expected_type) + let tokens = + self.apply_string_coercion(arg, &base_tokens, expected_type)?; + // INTLEN-1: a `usize` argument to an integer parameter is cast. + Ok(super::return_type_helpers::cast_usize_tokens( + arg, + expected_type, + tokens, + )) } else { // DEFECT-018 FIX: Auto-clone Identifier arguments in loop contexts // to prevent "use of moved value" errors on subsequent iterations diff --git a/src/backend/transpiler/return_type_helpers.rs b/src/backend/transpiler/return_type_helpers.rs index 9970ce67e..6e44d2390 100644 --- a/src/backend/transpiler/return_type_helpers.rs +++ b/src/backend/transpiler/return_type_helpers.rs @@ -350,10 +350,81 @@ pub fn expr_creates_or_returns_vec(expr: &Expr, block_exprs: &[Expr]) -> bool { /// integer type its tail cannot produce. (complexity: 2) #[must_use] pub fn returns_usize(body: &Expr) -> bool { - get_final_expression(body).is_some_and(|tail| { - matches!(&tail.kind, ExprKind::MethodCall { method, args, .. } - if args.is_empty() && matches!(method.as_str(), "len" | "count")) - }) + get_final_expression(body).is_some_and(is_usize_expr) +} + +/// INTLEN-1: `expr` is a zero-argument `len()` / `count()` call, a `usize` +/// in the transpiled Rust. (complexity: 2) +#[must_use] +pub fn is_usize_expr(expr: &Expr) -> bool { + matches!(&expr.kind, ExprKind::MethodCall { method, args, .. } + if args.is_empty() && matches!(method.as_str(), "len" | "count")) +} + +/// INTLEN-1: the signed/unsigned Rust integer type (other than `usize`) that +/// the Ruchy type name `ty` denotes, if any. (complexity: 2) +#[must_use] +pub fn usize_cast_target(ty: &str) -> Option<&'static str> { + match ty { + "int" | "i64" => Some("i64"), + "i32" => Some("i32"), + "u32" => Some("u32"), + "u64" => Some("u64"), + "isize" => Some("isize"), + _ => None, + } +} + +/// INTLEN-1: `tokens` (the transpiled `expr`) cast to the integer type `ty` +/// when `expr` is a `usize`; unchanged otherwise. (complexity: 2) +#[must_use] +pub fn cast_usize_tokens(expr: &Expr, ty: &str, tokens: TokenStream) -> TokenStream { + match usize_cast_target(ty) { + Some(target) if is_usize_expr(expr) => { + let target = proc_macro2::Ident::new(target, proc_macro2::Span::call_site()); + quote! { (#tokens as #target) } + } + _ => tokens, + } +} + +/// INTLEN-1: `body` with its `usize` tail wrapped in a cast to the declared +/// integer return type `ty`; `None` when no cast is needed. (complexity: 5) +#[must_use] +pub fn cast_usize_tail(body: &Expr, ty: &str) -> Option { + let target = usize_cast_target(ty)?; + let mut out = body.clone(); + let tail = final_expression_mut(&mut out)?; + if !is_usize_expr(tail) { + return None; + } + let inner = std::mem::replace(tail, Expr::new(ExprKind::Literal(Literal::Unit), tail.span)); + *tail = Expr::new( + ExprKind::TypeCast { + expr: Box::new(inner), + target_type: target.to_string(), + }, + body.span, + ); + Some(out) +} + +/// Mutable twin of `get_final_expression`. (complexity: 4) +fn final_expression_mut(expr: &mut Expr) -> Option<&mut Expr> { + let is_wrapper = matches!( + &expr.kind, + ExprKind::Block(_) | ExprKind::Let { .. } | ExprKind::LetPattern { .. } + ); + if !is_wrapper { + return Some(expr); + } + match &mut expr.kind { + ExprKind::Block(exprs) => exprs.last_mut().and_then(final_expression_mut), + ExprKind::Let { body, .. } | ExprKind::LetPattern { body, .. } => { + final_expression_mut(body) + } + _ => None, + } } /// Helper: Get the actual final expression, drilling through Let/Block wrappers @@ -463,6 +534,26 @@ where #[cfg(test)] mod tests { use super::*; + + #[test] + fn test_intlen_1_cast_usize_tail_and_tokens() { + let parse = |src: &str| { + crate::frontend::parser::Parser::new(src) + .parse() + .expect("parse") + }; + let len = parse("xs.len()"); + let cast = cast_usize_tail(&len, "int").expect("int tail is cast"); + assert!( + matches!(&cast.kind, ExprKind::TypeCast { target_type, .. } if target_type == "i64") + ); + assert!(cast_usize_tail(&len, "usize").is_none()); + assert!(cast_usize_tail(&parse("xs.first()"), "int").is_none()); + let tokens = cast_usize_tokens(&len, "i32", quote! { xs.len() }).to_string(); + assert_eq!(tokens.replace(' ', ""), "(xs.len()asi32)"); + let plain = cast_usize_tokens(&parse("x"), "int", quote! { x }).to_string(); + assert_eq!(plain, "x"); + } use crate::frontend::ast::{Expr, ExprKind, Literal, Span}; fn make_expr(kind: ExprKind) -> Expr { diff --git a/src/backend/transpiler/statements.rs b/src/backend/transpiler/statements.rs index 8c95646ea..c460f9fc3 100644 --- a/src/backend/transpiler/statements.rs +++ b/src/backend/transpiler/statements.rs @@ -241,6 +241,13 @@ impl Transpiler { ) } + /// INTLEN-1: `fun f(..) -> int { xs.len() }` gets its `usize` tail cast + /// to the declared integer type; `None` when the body needs no change. + fn usize_tail_cast_body(body: &Expr, return_type: Option<&Type>) -> Option { + let ty = Self::type_to_string(return_type?); + super::return_type_helpers::cast_usize_tail(body, &ty) + } + /// Transpile function with contract clauses (PMAT-001: Ruchy 5.0 Silver level) /// /// Contract clauses are transpiled to `debug_assert!` calls: @@ -259,6 +266,8 @@ impl Transpiler { contracts: &[crate::frontend::ast::ContractClause], ) -> Result { contract_pre_bce!(); + let cast_body = Self::usize_tail_cast_body(body, return_type); + let body = cast_body.as_ref().unwrap_or(body); if contracts.is_empty() { return self.transpile_function_impl( name, From b04a843f2c42c9f359121516d1ecb91c65992928 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:54:02 +0200 Subject: [PATCH 072/106] RHLGA-1: close PRINTLNFMT/INTLEN/COUNTITER/IDXCOMPOUND/IDXPUSHWB; file six more defects Filed, each reproduced at a5dc8398: PRINTSTR-1, FMTSPEC-1, EPRINTLN-1, FORMATFN-1, RETLEN-1, DICTIDX-1. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 14 +++- docs/roadmaps/roadmap.yaml | 133 +++++++++++++++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aef78d7ad..c64c1cd63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,7 +92,19 @@ Behaviour changes a program can observe are marked **(behaviour)**. mutating-method receivers on such a chain are emitted as places as well. - A method or function whose last expression is a unit-returning call (`self.items.push(x)`) infers no return type (UNITRET-1), and one ending in - `len()` or `count()` infers `-> usize` (LENRET-1). + `len()` or `count()` infers `-> usize` (LENRET-1). A `len()` or `count()` + that meets an `int` annotation, an `int` return tail or an `int` parameter + is cast (INTLEN-1). +- The interpreter accepts compound assignment to an index or field chain + (`v[1] += 5`, `o.items[0].z -= 1`) (IDXCOMPOUND-1), and in-place array + methods through an index chain (`t.rows[0].vals.push(9)`) are written back + (IDXPUSHWB-1). `count()` works on arrays and on `chars()`, `iter()` and + `bytes()` in the interpreter, and the transpiler no longer adds `.iter()` to + a receiver that is already an iterator (COUNTITER-1). +- **(behaviour)** A `print`/`println` whose first argument is a string literal + with any placeholder (`{:?}`, `{:>5}`, `{0}`, …) uses it as the format + string when compiled (PRINTLNFMT-1). Before, only `{}` was recognised, so + `println("{:?}", v)` printed the format string followed by the value. - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index e0b165cc9..58c7c6219 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4302,7 +4302,7 @@ roadmap: github_issue: null item_type: task title: 'PRINTLNFMT-1: println("{:?}", v) with one argument compiles to println!("{} {:?}", "{:?}", v) and prints the format string' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T10:30:00Z @@ -4323,7 +4323,7 @@ roadmap: github_issue: null item_type: task title: 'INTLEN-1: let n: int = v.len() (and -> int returning len()) emits i64 = usize without a cast (E0308)' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T10:30:00Z @@ -4343,7 +4343,7 @@ roadmap: github_issue: null item_type: task title: 'COUNTITER-1: count() gets .iter() appended even on an iterator (s.chars().count() -> s.chars().iter().count(), E0599); the interpreter has no array count' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T10:30:00Z @@ -4363,7 +4363,7 @@ roadmap: github_issue: null item_type: task title: 'IDXCOMPOUND-1: interpreter: v[1] += 5 fails with Invalid compound assignment target' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T10:30:00Z @@ -4383,7 +4383,7 @@ roadmap: github_issue: null item_type: task title: 'IDXPUSHWB-1: interpreter: t.rows[0].vals.push(9) is not written back to t (prints [1]; compiled prints [1, 9])' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T10:30:00Z @@ -4399,3 +4399,126 @@ roadmap: - runtime - filed-by:RHLGA-1 notes: null +- id: PRINTSTR-1 + github_issue: null + item_type: task + title: 'PRINTSTR-1: println(s) with a String/&str variable compiles to println!("{:?}", s) and prints quotes' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T11:30:00Z + updated: 2026-09-24T11:30:00Z + spec: null + acceptance_criteria: + - 'Repro: let t = s.to_string(); println(t) prints "hi" compiled, hi interpreted' + - 'Acceptance: rustc differential for println/print of str, String, format! results and numbers' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: FMTSPEC-1 + github_issue: null + item_type: task + title: 'FMTSPEC-1: interpreter println/format ignore width, precision and positional specs ({:>5}, {:.2}, {0})' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T11:30:00Z + updated: 2026-09-24T11:30:00Z + spec: null + acceptance_criteria: + - 'Repro: println("[{:>5}] [{:.2}]", 7, 3.14159) prints the placeholders literally' + - 'Acceptance: interpreter output equals rustc for fill/align/width/precision/positional/named specs' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 + notes: null +- id: EPRINTLN-1 + github_issue: null + item_type: task + title: 'EPRINTLN-1: interpreter: eprintln/eprint are undefined functions' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T11:30:00Z + updated: 2026-09-24T11:30:00Z + spec: null + acceptance_criteria: + - 'Acceptance: eprintln/eprint write to stderr in the interpreter, as compiled' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - filed-by:RHLGA-1 + notes: null +- id: FORMATFN-1 + github_issue: null + item_type: task + title: 'FORMATFN-1: format("{:?}", v) function form fails: rustc E0423 compiled, Unknown builtin __builtin_format__ interpreted' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T11:30:00Z + updated: 2026-09-24T11:30:00Z + spec: null + acceptance_criteria: + - 'Acceptance: format(...) function form equals format!(...) in both backends' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: RETLEN-1 + github_issue: null + item_type: task + title: 'RETLEN-1: return v.len() in a -> int function is not cast (E0308); INTLEN-1 covered only tails, lets and arguments' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T11:30:00Z + updated: 2026-09-24T11:30:00Z + spec: null + acceptance_criteria: + - 'Acceptance: rustc test for explicit return of len()/count() in int-returning functions' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null +- id: DICTIDX-1 + github_issue: null + item_type: task + title: 'DICTIDX-1: string-keyed dict index: transpiler emits d["k" as usize]; interpreter d["k"].push(2) errors Array index must be an integer' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T11:30:00Z + updated: 2026-09-24T11:30:00Z + spec: null + acceptance_criteria: + - 'Repro: let mut d = {"k": [1]}; d["k"].push(2); println("{:?}", d["k"])' + - 'Acceptance: dict read, assignment and in-place method through a string key agree between interpreter and rustc' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler + - filed-by:RHLGA-1 + notes: null From 1fb8e92bd071fbe63f4798ffc37e78375373db59 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 10:59:21 +0200 Subject: [PATCH 073/106] RHLGA-1: PRINTSTR-1 a string-typed single print argument uses Display - is_display_string: string literal/interpolation, format!/format(..), string-returning methods, and bindings/params recorded as String in variable_types (let, typed let, String/&str params); a non-string rebinding clears the record, each function starts fresh - single-arg println/print and the no-format-string multi-arg path use {} for those, {:?} otherwise - tests/g2b_transfix_2.rs: RED cases for PRINTSTR/FORMATFN/RETLEN/DICTIDX Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/backend/transpiler/bindings.rs | 2 + src/backend/transpiler/function_transpiler.rs | 3 + src/backend/transpiler/print_helpers.rs | 122 ++++++++-- .../transpiler/string_body_conversion.rs | 1 + tests/g2b_transfix_2.rs | 219 ++++++++++++++++++ 5 files changed, 330 insertions(+), 17 deletions(-) create mode 100644 tests/g2b_transfix_2.rs diff --git a/src/backend/transpiler/bindings.rs b/src/backend/transpiler/bindings.rs index 88aa571d1..a59bcf6b6 100644 --- a/src/backend/transpiler/bindings.rs +++ b/src/backend/transpiler/bindings.rs @@ -71,6 +71,7 @@ impl Transpiler { // Handle Rust reserved keywords by prefixing with r# // RHLGA-1 F7: `safe_ident` never r#-escapes `self`/`Self`/`super`/`crate`. let name_ident = Self::safe_ident(name); + self.track_string_binding(name, value, None); // Auto-detect mutability let effective_mutability = is_mutable @@ -263,6 +264,7 @@ impl Transpiler { ) -> Result { // RHLGA-1 F7: `safe_ident` never r#-escapes `self`/`Self`/`super`/`crate`. let name_ident = Self::safe_ident(name); + self.track_string_binding(name, value, type_annotation); // PARSER-073: Generate const/let keyword based on const attribute let is_mutable_var = is_mutable diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index aa76e4aac..05a2a14c7 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -39,6 +39,9 @@ impl Transpiler { modified_type_params.insert(0, "'a".to_string()); } + // PRINTSTR-1: string parameters print with Display. + self.track_string_params(params); + // TRANSPILER-004 FIX: Track String-typed parameters for proper concat transpilation // Before processing function body, register all String parameters in string_vars // This enables is_definitely_string() to detect them for `a + b` → `format!()` or `a + &b` diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index 60111d6d1..a8360ad59 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -9,7 +9,7 @@ //! **EXTREME TDD Round 64**: Extracted from statements.rs and mod.rs for modularization. use super::Transpiler; -use crate::frontend::ast::{Expr, ExprKind, Literal}; +use crate::frontend::ast::{Expr, ExprKind, Literal, Type, TypeKind}; use anyhow::Result; use proc_macro2::TokenStream; use quote::quote; @@ -109,8 +109,9 @@ impl Transpiler { if !matches!(&args[0].kind, ExprKind::Literal(Literal::String(_))) { let arg_tokens = self.transpile_expr(&args[0])?; // DEFECT-DICT-DETERMINISM FIX: Use Debug format with BTreeMap (deterministic) - // BTreeMap Debug format is sorted, so {:?} is safe and deterministic - let format_str = "{:?}"; + // BTreeMap Debug format is sorted, so {:?} is safe and deterministic. + // PRINTSTR-1: a known string prints with Display (no quotes). + let format_str = self.value_placeholder(&args[0]); return Ok(Some(quote! { #func_tokens!(#format_str, #arg_tokens) })); } } @@ -191,25 +192,15 @@ impl Transpiler { )) } else { // First argument is regular string, treat all as separate values - let format_parts: Vec<_> = args - .iter() - .map(|arg| match &arg.kind { - ExprKind::Literal(Literal::String(_)) => "{}", - _ => "{:?}", - }) - .collect(); + let format_parts: Vec<_> = + args.iter().map(|arg| self.value_placeholder(arg)).collect(); let format_str = format_parts.join(" "); Ok(Some(quote! { #func_tokens!(#format_str, #(#all_args),*) })) } } else { // No format string, treat all as separate values - let format_parts: Vec<_> = args - .iter() - .map(|arg| match &arg.kind { - ExprKind::Literal(Literal::String(_)) => "{}", - _ => "{:?}", - }) - .collect(); + let format_parts: Vec<_> = + args.iter().map(|arg| self.value_placeholder(arg)).collect(); let format_str = format_parts.join(" "); Ok(Some(quote! { #func_tokens!(#format_str, #(#all_args),*) })) } @@ -251,6 +242,103 @@ impl Transpiler { } } +/// PRINTSTR-1: the type tag `variable_types` holds for a string binding. +const STRING_VAR_TYPE: &str = "String"; + +/// PRINTSTR-1: methods whose result is a `String`/`&str` in the transpiled Rust. +const STRING_RESULT_METHODS: &[&str] = &[ + "to_string", + "to_uppercase", + "to_lowercase", + "trim", + "trim_start", + "trim_end", + "replace", + "repeat", +]; + +impl Transpiler { + /// PRINTSTR-1: `{}` for an expression known to be a string, `{:?}` for + /// anything else (a number prints the same either way). (complexity: 2) + pub(crate) fn value_placeholder(&self, expr: &Expr) -> &'static str { + if self.is_display_string(expr) { + "{}" + } else { + "{:?}" + } + } + + /// PRINTSTR-1: `expr` is a string in the transpiled Rust: a literal, + /// an interpolation, `format!`/`format(..)`, a string-returning method, + /// or a variable/parameter recorded as a string. (complexity: 6) + pub(crate) fn is_display_string(&self, expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Literal(Literal::String(_)) | ExprKind::StringInterpolation { .. } => true, + ExprKind::Macro { name, .. } | ExprKind::MacroInvocation { name, .. } => { + name.trim_end_matches('!') == "format" + } + ExprKind::Call { func, args } => self.is_format_fn_call(func, args), + ExprKind::MethodCall { method, .. } => STRING_RESULT_METHODS.contains(&method.as_str()), + ExprKind::Identifier(name) => self + .variable_types + .borrow() + .get(name) + .is_some_and(|t| t == STRING_VAR_TYPE), + _ => false, + } + } + + /// PRINTSTR-1: record whether the binding `name` holds a string, from its + /// annotation when present, else from its value. A rebinding to a + /// non-string clears the record. (complexity: 2) + pub(crate) fn track_string_binding(&self, name: &str, value: &Expr, ty: Option<&Type>) { + let is_string = ty.map_or_else(|| self.is_display_string(value), is_string_annotation); + self.set_string_var(name, is_string); + } + + /// PRINTSTR-1: parameters of the function being transpiled; string + /// records of earlier functions are dropped first. (complexity: 2) + pub(crate) fn track_string_params(&self, params: &[crate::frontend::ast::Param]) { + self.variable_types + .borrow_mut() + .retain(|_, t| t != STRING_VAR_TYPE); + for param in params { + self.set_string_var(¶m.name(), is_string_annotation(¶m.ty)); + } + } + + /// PRINTSTR-1: set or clear the string record of `name`. (complexity: 3) + fn set_string_var(&self, name: &str, is_string: bool) { + let mut types = self.variable_types.borrow_mut(); + if is_string { + types.insert(name.to_string(), STRING_VAR_TYPE.to_string()); + } else if types.get(name).is_some_and(|t| t == STRING_VAR_TYPE) { + types.remove(name); + } + } + + /// FORMATFN-1: `func(args)` is the function form of `format!`: `format` + /// with a string-literal format argument and no user `format` function. + /// (complexity: 3) + pub(crate) fn is_format_fn_call(&self, func: &Expr, args: &[Expr]) -> bool { + matches!(&func.kind, ExprKind::Identifier(n) if n == "format") + && matches!( + args.first().map(|a| &a.kind), + Some(ExprKind::Literal(Literal::String(_))) + ) + && !self.function_signatures.contains_key("format") + } +} + +/// PRINTSTR-1: `ty` is `String`, `str` or a reference to one. (complexity: 3) +fn is_string_annotation(ty: &Type) -> bool { + match &ty.kind { + TypeKind::Named(name) => matches!(name.as_str(), "String" | "str" | "&str"), + TypeKind::Reference { inner, .. } => is_string_annotation(inner), + _ => false, + } +} + /// PRINTLNFMT-1: `s` contains at least one `{…}` placeholder (`{}`, `{:?}`, /// `{name}`, `{0}`, `{:>5}`, ...), so it is a format string. `{{` escapes /// do not count. (complexity: 4) diff --git a/src/backend/transpiler/string_body_conversion.rs b/src/backend/transpiler/string_body_conversion.rs index 6ddc92196..e320be8f1 100644 --- a/src/backend/transpiler/string_body_conversion.rs +++ b/src/backend/transpiler/string_body_conversion.rs @@ -109,6 +109,7 @@ impl Transpiler { ) -> Result { let name_ident = format_ident!("{}", name); let is_mutable_var = self.check_mutability(name, is_mutable, let_body); + self.track_string_binding(name, value, type_annotation); let value_tokens = self.convert_value_for_string_context(name, value, type_annotation, is_mutable_var)?; let let_body_tokens = self.transpile_expr(let_body)?; diff --git a/tests/g2b_transfix_2.rs b/tests/g2b_transfix_2.rs new file mode 100644 index 000000000..39d2f1b5f --- /dev/null +++ b/tests/g2b_transfix_2.rs @@ -0,0 +1,219 @@ +//! RHLGA-1 transpiler fixes (second batch), each checked end to end: +//! `ruchy transpile`, `ruchy compile` (rustc), run the binary, compare exact +//! stdout. +//! +//! - PRINTSTR-1: a single string-typed argument to print/println (a string +//! literal binding, `to_string()`, `format!`, a `String` parameter) prints +//! with `{}`, not `{:?}`, so the compiled program prints no quotes. +//! - FORMATFN-1: the function form `format(...)` transpiles like `format!(...)`. +//! - RETLEN-1: an explicit `return ` in an int-returning +//! function is cast, including early returns inside `if` branches. +//! - DICTIDX-1: a string key indexes a dict literal as a map (read, index +//! assignment, in-place method); `as usize` is only for integer indices. +//! +//! PRINTSTR-1 and RETLEN-1 also require `ruchy run` to agree; the FORMATFN-1 +//! and DICTIDX-1 interpreter halves are tracked separately. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +/// (stdout, stderr) of a command that must succeed. +fn output_of(mut cmd: Command, what: &str) -> (String, String) { + let out = cmd.output().expect("spawn command"); + let stdout = String::from_utf8(out.stdout).expect("utf8 stdout"); + let stderr = String::from_utf8(out.stderr).expect("utf8 stderr"); + assert!(out.status.success(), "{what} failed:\n{stdout}{stderr}"); + (stdout, stderr) +} + +fn transpile(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + output_of(cmd, &format!("ruchy transpile of:\n{src}\n")).0 +} + +/// `ruchy compile` the source, run the binary, return (stdout, stderr). +fn compile_and_run(src: &str) -> (String, String) { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let bin = dir.path().join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(90)); + output_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + output_of(run, "compiled binary") +} + +fn interpret(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + output_of(cmd, &format!("ruchy run of:\n{src}\n")).0 +} + +fn main_with(body: &str) -> String { + format!("fun main() {{\n{body}\n}}\n") +} + +/// Compiled stdout is `expected`; with `interp`, `ruchy run` agrees. +fn check(src: &str, expected: &str, interp: bool) { + let (stdout, _) = compile_and_run(src); + assert_eq!( + stdout, + expected, + "compiled stdout of:\n{src}\n{}", + transpile(src) + ); + if interp { + assert_eq!(interpret(src), expected, "interpreter stdout of:\n{src}"); + } +} + +// ---------------------------------------------------------------- PRINTSTR-1 + +#[test] +fn test_printstr_1_literal_to_string_and_format_bindings() { + let src = main_with( + r#" let s = "hi" + let t = s.to_string() + let u = format!("{}-{}", s, 1) + println(s) + println(t) + println(u)"#, + ); + check(&src, "hi\nhi\nhi-1\n", true); +} + +#[test] +fn test_printstr_1_string_parameter() { + let src = "fun greet(name: String) {\n println(name)\n}\n\ + fun main() {\n greet(\"bob\".to_string())\n println(\"done\")\n}\n"; + check(src, "bob\ndone\n", true); +} + +#[test] +fn test_printstr_1_string_method_result() { + check( + &main_with(r#" println("a b".to_uppercase())"#), + "A B\n", + true, + ); +} + +#[test] +fn test_printstr_1_numbers_and_vecs_unchanged() { + let src = main_with( + r#" let n = 42 + let v = [1, 2] + println(n) + println(v)"#, + ); + check(&src, "42\n[1, 2]\n", true); +} + +// ---------------------------------------------------------------- FORMATFN-1 + +#[test] +fn test_formatfn_1_debug_placeholder() { + let src = main_with( + r#" let f = format("{:?}", [1, 2]) + println(f)"#, + ); + check(&src, "[1, 2]\n", false); +} + +#[test] +fn test_formatfn_1_display_placeholders() { + let src = main_with( + r#" let g = format("{}-{}", "a", 3) + println(g)"#, + ); + check(&src, "a-3\n", false); +} + +// ---------------------------------------------------------------- RETLEN-1 + +#[test] +fn test_retlen_1_explicit_return_len() { + let src = "fun n(v: Vec) -> int {\n return v.len()\n}\n\ + fun main() {\n println(n([1, 2, 3]))\n}\n"; + check(src, "3\n", true); +} + +#[test] +fn test_retlen_1_early_return_in_if_branch() { + let src = "fun m(v: Vec) -> int {\n if v.len() > 2 {\n return v.len()\n }\n return 0\n}\n\ + fun main() {\n println(m([1, 2, 3]))\n println(m([1]))\n}\n"; + check(src, "3\n0\n", true); +} + +// ---------------------------------------------------------------- DICTIDX-1 + +#[test] +fn test_dictidx_1_push_through_string_key() { + let src = main_with( + r#" let mut d = {"k": [1]} + d["k"].push(2) + println("{:?}", d["k"])"#, + ); + check(&src, "[1, 2]\n", false); +} + +#[test] +fn test_dictidx_1_read_string_key() { + let src = main_with( + r#" let d = {"a": [1, 2]} + println("{:?}", d["a"])"#, + ); + check(&src, "[1, 2]\n", false); +} + +#[test] +fn test_dictidx_1_assign_string_key() { + let src = main_with( + r#" let mut d = {"k": [1]} + d["k"] = [7, 8] + println("{:?}", d["k"])"#, + ); + check(&src, "[7, 8]\n", false); +} + +#[test] +fn test_dictidx_1_debug_print_map() { + let src = main_with( + r#" let d = {"a": [1]} + println("{:?}", d)"#, + ); + check(&src, "{\"a\": [1]}\n", false); +} + +#[test] +fn test_dictidx_1_integer_index_unchanged() { + let src = main_with( + r#" let v = [1, 2, 3] + let i = 1 + println(v[i])"#, + ); + check(&src, "2\n", true); +} From cf547eec9f724df34cb73afc9db01c8480435670 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:00:36 +0200 Subject: [PATCH 074/106] RHLGA-1: FORMATFN-1 format(...) transpiles like format!(...) - try_transpile_format_fn: `format` called with a string-literal format argument (and no user fn `format`) becomes the format! macro, so the placeholders are handled exactly as for format!; before, rustc E0423 - a format(..) result is a string for PRINTSTR-1 printing Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 512 ++++++++++--------- src/backend/transpiler/call_transpilation.rs | 3 + src/backend/transpiler/print_helpers.rs | 35 +- 3 files changed, 293 insertions(+), 257 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 9a4e6f766..6b011ab05 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T08:46:05.534410689Z", + "created_at": "2026-09-24T09:27:43.265255009Z", "git_context": null, "files": { "./build.rs": { @@ -3040,48 +3040,48 @@ }, "./src/backend/transpiler/bindings.rs": { "content_hash": [ - 118, - 130, - 37, + 159, + 138, + 50, + 203, + 85, + 188, + 229, + 131, + 127, + 147, + 9, + 181, + 89, + 114, + 227, + 14, 102, - 47, - 120, - 163, - 26, - 245, - 78, - 230, - 60, - 64, - 24, - 108, - 151, + 81, + 178, + 83, + 105, + 55, + 223, + 137, + 144, + 20, 205, - 248, + 212, 57, - 8, - 174, - 127, - 195, - 159, - 96, - 176, - 21, - 251, - 4, - 128, - 250, - 118 + 118, + 215, + 84 ], "score": { - "structural_complexity": 3.5, + "structural_complexity": 2.5, "semantic_complexity": 20.0, - "duplication_ratio": 14.004329, + "duplication_ratio": 14.02763, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.50433, + "total": 76.52763, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3093,15 +3093,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 96 duplicate code patterns" + "issue": "Found 97 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.9956713, + "amount": 5.97237, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 30.0%" + "issue": "Code duplication: 29.9%" }, { "source_metric": "StructuralComplexity", @@ -3109,15 +3109,15 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 50" + "issue": "High cognitive complexity: 51" }, { "source_metric": "StructuralComplexity", - "amount": 11.5, + "amount": 12.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 53" + "issue": "High cyclomatic complexity: 55" } ], "critical_defects_count": 0, @@ -3491,48 +3491,48 @@ }, "./src/backend/transpiler/call_helpers.rs": { "content_hash": [ - 142, + 18, + 249, + 134, + 121, + 172, + 78, + 54, + 115, 161, - 87, - 65, - 171, - 144, + 93, + 228, 43, - 39, - 126, - 162, - 17, - 185, - 187, - 132, + 57, + 133, + 52, + 244, + 131, + 217, + 74, 156, - 98, - 47, - 116, - 253, - 101, - 27, - 227, - 191, - 125, - 106, - 123, - 189, - 102, - 22, - 164, - 61, - 255 + 255, + 157, + 2, + 230, + 179, + 226, + 142, + 117, + 246, + 135, + 33, + 88 ], "score": { "structural_complexity": 22.0, "semantic_complexity": 20.0, - "duplication_ratio": 14.023668, + "duplication_ratio": 14.127907, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 96.02367, + "total": 96.12791, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -3548,11 +3548,11 @@ }, { "source_metric": "Duplication", - "amount": 5.976331, + "amount": 5.8720927, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.9%" + "issue": "Code duplication: 29.4%" }, { "source_metric": "StructuralComplexity", @@ -6194,48 +6194,48 @@ }, "./src/backend/transpiler/function_transpiler.rs": { "content_hash": [ - 143, - 35, - 192, - 126, - 148, - 80, - 189, - 246, - 161, - 158, - 66, - 171, - 222, - 37, - 121, - 34, - 245, - 68, - 226, - 108, - 157, + 125, + 69, + 120, + 84, + 79, + 33, + 79, 7, - 209, - 190, - 9, - 148, - 102, - 42, - 17, - 150, - 147, - 145 + 34, + 87, + 6, + 21, + 174, + 92, + 71, + 37, + 101, + 46, + 249, + 80, + 30, + 162, + 13, + 135, + 109, + 119, + 109, + 180, + 152, + 240, + 76, + 52 ], "score": { "structural_complexity": 17.6, "semantic_complexity": 20.0, - "duplication_ratio": 15.256917, + "duplication_ratio": 15.266272, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.85692, + "total": 92.86627, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -6251,7 +6251,7 @@ }, { "source_metric": "Duplication", - "amount": 4.743083, + "amount": 4.733728, "applied_to": [ "Duplication" ], @@ -7679,49 +7679,49 @@ }, "./src/backend/transpiler/print_helpers.rs": { "content_hash": [ - 134, - 150, - 19, - 235, - 175, - 179, - 42, - 141, - 12, - 110, + 210, + 148, + 188, + 156, + 163, + 209, 185, - 112, - 79, - 252, - 12, - 195, - 131, - 252, - 253, - 51, - 236, - 60, - 217, - 194, - 201, - 243, - 1, - 1, - 34, - 157, - 211, - 182 + 142, + 244, + 174, + 50, + 14, + 140, + 46, + 74, + 186, + 71, + 164, + 16, + 150, + 20, + 89, + 90, + 117, + 245, + 4, + 203, + 203, + 71, + 52, + 154, + 196 ], "score": { - "structural_complexity": 18.4, + "structural_complexity": 14.4, "semantic_complexity": 20.0, - "duplication_ratio": 14.028629, + "duplication_ratio": 14.727273, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 92.428635, - "grade": "A", + "total": 89.12727, + "grade": "A-", "confidence": 1.0, "language": "Rust", "file_path": "./src/backend/transpiler/print_helpers.rs", @@ -7732,15 +7732,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 44 duplicate code patterns" + "issue": "Found 41 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.97137, + "amount": 5.272727, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 29.9%" + "issue": "Code duplication: 26.4%" }, { "source_metric": "StructuralComplexity", @@ -7749,6 +7749,14 @@ "StructuralComplexity" ], "issue": "High cognitive complexity: 37" + }, + { + "source_metric": "StructuralComplexity", + "amount": 4.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 38" } ], "critical_defects_count": 0, @@ -7940,48 +7948,48 @@ }, "./src/backend/transpiler/return_type_helpers.rs": { "content_hash": [ - 64, - 203, - 87, - 30, - 216, - 227, - 209, + 67, + 253, + 83, + 165, + 67, + 246, + 12, + 197, 5, - 98, - 48, - 214, - 192, - 210, - 112, - 156, + 10, + 21, + 106, + 245, + 110, 227, - 179, - 254, - 68, - 214, - 176, - 176, - 66, - 248, - 103, - 225, - 29, - 43, - 181, - 131, - 167, - 120 + 174, + 162, + 224, + 34, + 60, + 172, + 78, + 252, + 81, + 217, + 55, + 107, + 189, + 33, + 223, + 246, + 118 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.506493, + "duplication_ratio": 17.588654, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.50649, + "total": 77.58865, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -7993,15 +8001,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 58 duplicate code patterns" + "issue": "Found 63 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 2.4935064, + "amount": 2.4113474, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 12.5%" + "issue": "Code duplication: 12.1%" }, { "source_metric": "StructuralComplexity", @@ -8009,7 +8017,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 52" + "issue": "High cognitive complexity: 57" }, { "source_metric": "StructuralComplexity", @@ -8017,7 +8025,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 63" + "issue": "High cyclomatic complexity: 76" } ], "critical_defects_count": 0, @@ -8035,48 +8043,48 @@ }, "./src/backend/transpiler/statements.rs": { "content_hash": [ - 155, - 27, + 158, + 138, 50, - 207, - 222, - 31, - 223, - 212, - 13, - 166, - 74, - 200, - 33, - 117, - 220, - 94, - 154, - 203, - 38, - 180, - 13, - 44, - 253, - 193, - 204, - 17, - 148, - 38, + 2, + 51, + 208, + 147, + 170, + 90, + 190, + 233, + 155, + 211, + 89, + 230, + 35, + 210, + 63, 15, - 85, - 144, - 149 + 185, + 219, + 121, + 193, + 45, + 106, + 194, + 23, + 244, + 82, + 225, + 91, + 44 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.006859, + "duplication_ratio": 15.047619, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 90.91533, + "total": 90.952385, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -8092,11 +8100,11 @@ }, { "source_metric": "Duplication", - "amount": 4.993141, + "amount": 4.952381, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 25.0%" + "issue": "Code duplication: 24.8%" } ], "critical_defects_count": 0, @@ -8209,48 +8217,48 @@ }, "./src/backend/transpiler/string_body_conversion.rs": { "content_hash": [ - 41, - 197, - 31, + 155, + 17, + 221, + 238, + 172, + 21, + 60, + 3, + 37, + 2, 210, - 70, - 247, - 84, - 216, - 230, - 84, - 225, - 148, - 110, - 87, - 57, - 232, - 217, - 84, - 85, - 44, - 192, + 118, + 46, + 122, + 170, 94, - 61, - 216, - 0, - 207, - 4, - 110, - 81, - 142, - 41, - 90 + 198, + 60, + 86, + 245, + 236, + 202, + 146, + 195, + 160, + 2, + 30, + 25, + 115, + 145, + 143, + 184 ], "score": { "structural_complexity": 24.7, "semantic_complexity": 20.0, - "duplication_ratio": 15.369262, + "duplication_ratio": 15.378486, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 90.97205, + "total": 90.980446, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -8266,11 +8274,11 @@ }, { "source_metric": "Duplication", - "amount": 4.6307383, + "amount": 4.6215143, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 23.2%" + "issue": "Code duplication: 23.1%" }, { "source_metric": "StructuralComplexity", @@ -45046,11 +45054,11 @@ }, "summary": { "total_files": 530, - "avg_score": 90.2847, + "avg_score": 90.277084, "grade_distribution": { "A+": 195, - "A": 175, - "A-": 33, + "A": 174, + "A-": 34, "B+": 29, "B": 69, "B-": 26, diff --git a/src/backend/transpiler/call_transpilation.rs b/src/backend/transpiler/call_transpilation.rs index 092977820..c390cf1df 100644 --- a/src/backend/transpiler/call_transpilation.rs +++ b/src/backend/transpiler/call_transpilation.rs @@ -143,6 +143,9 @@ impl Transpiler { if let Some(result) = self.try_transpile_print_macro(func_tokens, base_name, args)? { return Ok(Some(result)); } + if let Some(result) = self.try_transpile_format_fn(name, args)? { + return Ok(Some(result)); + } // len(x) → x.len() if base_name == "len" && args.len() == 1 { diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index a8360ad59..16c91fdd0 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -317,17 +317,42 @@ impl Transpiler { } } - /// FORMATFN-1: `func(args)` is the function form of `format!`: `format` - /// with a string-literal format argument and no user `format` function. - /// (complexity: 3) - pub(crate) fn is_format_fn_call(&self, func: &Expr, args: &[Expr]) -> bool { - matches!(&func.kind, ExprKind::Identifier(n) if n == "format") + /// FORMATFN-1: `func(args)` is the function form of `format!`. + /// (complexity: 2) + fn is_format_fn_call(&self, func: &Expr, args: &[Expr]) -> bool { + matches!(&func.kind, ExprKind::Identifier(n) if self.is_format_fn(n, args)) + } + + /// FORMATFN-1: `name(args)` is `format` with a string-literal format + /// argument, and no user function named `format` exists. (complexity: 3) + fn is_format_fn(&self, name: &str, args: &[Expr]) -> bool { + name == "format" && matches!( args.first().map(|a| &a.kind), Some(ExprKind::Literal(Literal::String(_))) ) && !self.function_signatures.contains_key("format") } + + /// FORMATFN-1: `format(fmt, ..)` transpiles exactly like `format!(fmt, ..)`. + /// (complexity: 2) + pub(crate) fn try_transpile_format_fn( + &self, + name: &str, + args: &[Expr], + ) -> Result> { + if !self.is_format_fn(name, args) { + return Ok(None); + } + let macro_expr = Expr::new( + ExprKind::MacroInvocation { + name: "format".to_string(), + args: args.to_vec(), + }, + args[0].span, + ); + self.transpile_expr(¯o_expr).map(Some) + } } /// PRINTSTR-1: `ty` is `String`, `str` or a reference to one. (complexity: 3) From e311c566a0298cc0e29d54f7ddacd7b0329bd93c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:02:34 +0200 Subject: [PATCH 075/106] RHLGA-1: RETLEN-1 explicit return of len()/count() casts to the int return type - cast_usize_return: `return ` in a function whose declared return type is int/i64/i32/u32/u64/isize is cast (reuses INTLEN-1's cast_usize_tokens), including early returns inside if branches - RETLEN tests pass a String argument: an array literal passed to a Vec parameter gets no .to_vec() (separate defect) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 152 +++++++++--------- .../transpiler/dispatcher_helpers/misc.rs | 15 ++ tests/g2b_transfix_2.rs | 10 +- 3 files changed, 97 insertions(+), 80 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 6b011ab05..723010d83 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T09:27:43.265255009Z", + "created_at": "2026-09-24T09:27:45.401004370Z", "git_context": null, "files": { "./build.rs": { @@ -3578,48 +3578,48 @@ }, "./src/backend/transpiler/call_transpilation.rs": { "content_hash": [ - 208, - 202, - 77, - 167, - 216, - 163, - 189, 117, - 245, - 216, - 69, - 74, - 116, + 197, + 49, 33, - 102, - 19, - 110, - 212, - 72, - 163, 52, - 237, - 163, + 58, + 131, + 102, + 3, + 77, + 101, + 83, 200, - 183, - 199, - 60, - 54, - 253, - 203, - 162, - 216 + 159, + 224, + 81, + 89, + 164, + 76, + 197, + 186, + 0, + 237, + 231, + 109, + 245, + 157, + 8, + 182, + 120, + 127, + 46 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.023256, + "duplication_ratio": 16.013905, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.023254, + "total": 76.0139, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -3643,7 +3643,7 @@ }, { "source_metric": "Duplication", - "amount": 3.9767442, + "amount": 3.986095, "applied_to": [ "Duplication" ], @@ -3655,7 +3655,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 72" + "issue": "High cognitive complexity: 73" }, { "source_metric": "StructuralComplexity", @@ -3663,7 +3663,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 71" + "issue": "High cyclomatic complexity: 72" } ], "critical_defects_count": 0, @@ -7679,48 +7679,48 @@ }, "./src/backend/transpiler/print_helpers.rs": { "content_hash": [ - 210, - 148, - 188, - 156, - 163, - 209, - 185, - 142, - 244, - 174, - 50, - 14, - 140, - 46, - 74, - 186, - 71, - 164, - 16, - 150, - 20, - 89, - 90, - 117, + 56, + 99, + 216, + 78, + 192, + 199, + 181, + 216, 245, - 4, - 203, - 203, - 71, - 52, - 154, - 196 + 225, + 66, + 3, + 109, + 96, + 167, + 137, + 208, + 230, + 214, + 87, + 35, + 140, + 15, + 115, + 244, + 121, + 72, + 215, + 64, + 207, + 232, + 64 ], "score": { - "structural_complexity": 14.4, + "structural_complexity": 13.6, "semantic_complexity": 20.0, - "duplication_ratio": 14.727273, + "duplication_ratio": 14.807017, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 89.12727, + "total": 88.40701, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -7732,31 +7732,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 41 duplicate code patterns" + "issue": "Found 43 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 5.272727, + "amount": 5.1929827, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 26.4%" + "issue": "Code duplication: 26.0%" }, { "source_metric": "StructuralComplexity", - "amount": 6.6000004, + "amount": 6.9, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 37" + "issue": "High cognitive complexity: 38" }, { "source_metric": "StructuralComplexity", - "amount": 4.0, + "amount": 4.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 38" + "issue": "High cyclomatic complexity: 39" } ], "critical_defects_count": 0, @@ -45054,7 +45054,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.277084, + "avg_score": 90.27571, "grade_distribution": { "A+": 195, "A": 174, diff --git a/src/backend/transpiler/dispatcher_helpers/misc.rs b/src/backend/transpiler/dispatcher_helpers/misc.rs index 298456af1..e2f4e45a9 100644 --- a/src/backend/transpiler/dispatcher_helpers/misc.rs +++ b/src/backend/transpiler/dispatcher_helpers/misc.rs @@ -54,6 +54,7 @@ impl Transpiler { // In match arms, return is an expression and shouldn't have trailing semicolon if let Some(val_expr) = value { let val_tokens = self.transpile_expr(val_expr)?; + let val_tokens = self.cast_usize_return(val_expr, val_tokens); Ok(quote! { return #val_tokens }) } else { Ok(quote! { return }) @@ -63,6 +64,20 @@ impl Transpiler { } } + /// RETLEN-1: an explicit `return ` in a function whose + /// declared return type is an integer type is cast to it (INTLEN-1 casts + /// only tails, lets and arguments). (complexity: 2) + fn cast_usize_return(&self, value: &Expr, tokens: TokenStream) -> TokenStream { + match self.current_function_return_type.borrow().as_ref() { + Some(ty) => crate::backend::transpiler::return_type_helpers::cast_usize_tokens( + value, + &Self::type_to_string(ty), + tokens, + ), + None => tokens, + } + } + pub(in crate::backend::transpiler) fn make_break_continue( is_break: bool, label: Option<&String>, diff --git a/tests/g2b_transfix_2.rs b/tests/g2b_transfix_2.rs index 39d2f1b5f..1b6e43e6b 100644 --- a/tests/g2b_transfix_2.rs +++ b/tests/g2b_transfix_2.rs @@ -156,15 +156,17 @@ fn test_formatfn_1_display_placeholders() { #[test] fn test_retlen_1_explicit_return_len() { - let src = "fun n(v: Vec) -> int {\n return v.len()\n}\n\ - fun main() {\n println(n([1, 2, 3]))\n}\n"; + // A String argument: an array literal passed to a `Vec` parameter is a + // separate defect (no `.to_vec()`), kept out of this case. + let src = "fun n(s: String) -> int {\n return s.len()\n}\n\ + fun main() {\n println(n(\"abc\".to_string()))\n}\n"; check(src, "3\n", true); } #[test] fn test_retlen_1_early_return_in_if_branch() { - let src = "fun m(v: Vec) -> int {\n if v.len() > 2 {\n return v.len()\n }\n return 0\n}\n\ - fun main() {\n println(m([1, 2, 3]))\n println(m([1]))\n}\n"; + let src = "fun m(s: String) -> int {\n if s.len() > 2 {\n return s.len()\n }\n return 0\n}\n\ + fun main() {\n println(m(\"abc\".to_string()))\n println(m(\"a\".to_string()))\n}\n"; check(src, "3\n0\n", true); } From 7b0c23939d2079082ad58e014c8683e0fba4a901 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:20:07 +0200 Subject: [PATCH 076/106] RHLGA-1: DICTIDX-1 string-keyed dict index reads, assigns and mutates the map - a dict literal whose values are all list literals is a BTreeMap of Vecs (values were stringified, and arrays have no Display); other dicts keep BTreeMap - place position: d["k"] is (*d.get_mut("k").expect(..)), so d["k"].push(x) mutates the entry; only an integer index gets `as usize` - d["k"] = v is d.insert("k".to_string(), v), v converted as a dict value - {:?} of such a map prints {"a": [1]} Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 74 +++++++++---------- src/backend/transpiler/expressions.rs | 38 ++++++++++ .../expressions_helpers/collections.rs | 50 +++++++++++++ 3 files changed, 125 insertions(+), 37 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 723010d83..9d4810717 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T09:27:45.401004370Z", + "created_at": "2026-09-24T09:27:46.800038503Z", "git_context": null, "files": { "./build.rs": { @@ -4923,48 +4923,48 @@ }, "./src/backend/transpiler/dispatcher_helpers/misc.rs": { "content_hash": [ - 203, - 28, - 158, - 32, - 181, - 92, - 220, - 201, - 243, - 106, - 159, - 211, - 166, - 47, - 102, - 57, - 133, - 247, - 187, - 111, - 74, - 176, - 226, - 55, - 211, - 102, - 225, - 55, - 143, - 190, + 208, + 120, + 52, + 107, + 116, + 255, + 179, + 104, + 151, + 43, + 77, + 44, + 193, + 213, + 81, + 150, + 43, + 245, + 255, + 117, + 138, + 109, + 254, 122, - 157 + 185, + 150, + 255, + 247, + 15, + 88, + 42, + 160 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 11.2483225, + "duplication_ratio": 11.375662, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 96.24832, + "total": 96.37566, "grade": "A+", "confidence": 1.0, "language": "Rust", @@ -4980,11 +4980,11 @@ }, { "source_metric": "Duplication", - "amount": 8.7516775, + "amount": 8.624338, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 43.8%" + "issue": "Code duplication: 43.1%" } ], "critical_defects_count": 0, @@ -45054,7 +45054,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.27571, + "avg_score": 90.27595, "grade_distribution": { "A+": 195, "A": 174, diff --git a/src/backend/transpiler/expressions.rs b/src/backend/transpiler/expressions.rs index acc02a9c6..61a3c0475 100644 --- a/src/backend/transpiler/expressions.rs +++ b/src/backend/transpiler/expressions.rs @@ -144,6 +144,11 @@ impl Transpiler { } } + // DICTIDX-1: `d["k"] = v` inserts into the map + if let Some(tokens) = self.try_transpile_map_key_assign(target, value)? { + return Ok(tokens); + } + // Standard assignment (no deadlock risk) let value_tokens = self.transpile_expr(value)?; @@ -152,6 +157,27 @@ impl Transpiler { Ok(quote! { #target_tokens = #value_tokens }) } + /// DICTIDX-1: `map["k"] = v` is `map.insert("k".to_string(), v)`; the + /// value is converted as a dict-literal value is. (complexity: 2) + fn try_transpile_map_key_assign( + &self, + target: &Expr, + value: &Expr, + ) -> Result> { + let ExprKind::IndexAccess { object, index } = &target.kind else { + return Ok(None); + }; + if !is_string_key(index) { + return Ok(None); + } + let obj_tokens = self.transpile_place(object)?; + let key_tokens = self.transpile_expr(index)?; + let value_tokens = self.map_value_tokens(value, false)?; + Ok(Some( + quote! { #obj_tokens.insert(#key_tokens.to_string(), #value_tokens) }, + )) + } + /// Check if an expression references a specific variable name fn expr_references_var(expr: &Expr, var_name: &str) -> bool { match &expr.kind { @@ -297,6 +323,12 @@ impl Transpiler { ExprKind::IndexAccess { object, index } => { let obj_tokens = self.transpile_place(object)?; let idx_tokens = self.transpile_expr(index)?; + if is_string_key(index) { + // DICTIDX-1: a string key reaches the map entry in place + return Ok( + quote! { (*#obj_tokens.get_mut(#idx_tokens).expect("Key not found")) }, + ); + } Ok(quote! { #obj_tokens[#idx_tokens as usize] }) } ExprKind::FieldAccess { object, field } => { @@ -2034,3 +2066,9 @@ mod tests { assert!(code.contains("42"), "Literal should pass through unchanged"); } } + +/// DICTIDX-1: `index` is a string literal, i.e. a map key rather than a +/// position (only a position gets the `as usize` cast). (complexity: 1) +pub(crate) fn is_string_key(index: &Expr) -> bool { + matches!(&index.kind, ExprKind::Literal(Literal::String(_))) +} diff --git a/src/backend/transpiler/expressions_helpers/collections.rs b/src/backend/transpiler/expressions_helpers/collections.rs index 868331d28..8d48ae420 100644 --- a/src/backend/transpiler/expressions_helpers/collections.rs +++ b/src/backend/transpiler/expressions_helpers/collections.rs @@ -149,6 +149,10 @@ impl Transpiler { &self, fields: &[crate::frontend::ast::ObjectField], ) -> Result { + // DICTIDX-1: a dict whose values are all lists maps to `Vec`s + if list_valued_fields(fields) { + return self.transpile_list_valued_object(fields); + } let field_tokens = self.collect_hashmap_field_tokens(fields)?; // DEFECT-DICT-DETERMINISM FIX: Use BTreeMap for deterministic key ordering // BTreeMap maintains sorted order, HashMap has non-deterministic iteration order @@ -160,6 +164,42 @@ impl Transpiler { } }) } + /// DICTIDX-1: `{"k": [..], ..}` is a `BTreeMap>`, so + /// `d["k"].push(x)` and `{:?}` work on the real list. (complexity: 3) + fn transpile_list_valued_object( + &self, + fields: &[crate::frontend::ast::ObjectField], + ) -> Result { + use crate::frontend::ast::ObjectField; + let mut inserts = Vec::new(); + for field in fields { + if let ObjectField::KeyValue { key, value } = field { + let value_tokens = self.map_value_tokens(value, true)?; + inserts.push(quote! { map.insert(#key.to_string(), #value_tokens); }); + } + } + Ok(quote! { + { + let mut map: std::collections::BTreeMap = std::collections::BTreeMap::new(); + #(#inserts)* + map + } + }) + } + + /// DICTIDX-1: a dict value as stored in the map: a list literal is a + /// `Vec`; anything else is kept as is when `typed`, else stringified + /// (the `BTreeMap` of a dict with non-list values). + /// (complexity: 3) + pub(crate) fn map_value_tokens(&self, value: &Expr, typed: bool) -> Result { + let tokens = self.transpile_expr(value)?; + Ok(match &value.kind { + ExprKind::List(_) => quote! { #tokens.to_vec() }, + _ if typed => tokens, + _ => quote! { (#tokens).to_string() }, + }) + } + fn collect_hashmap_field_tokens( &self, fields: &[crate::frontend::ast::ObjectField], @@ -902,3 +942,13 @@ mod tests { assert!(result_str.contains("insert")); } } + +/// DICTIDX-1: a non-empty dict literal whose values are all list literals +/// (no spread). (complexity: 3) +fn list_valued_fields(fields: &[crate::frontend::ast::ObjectField]) -> bool { + use crate::frontend::ast::ObjectField; + !fields.is_empty() + && fields.iter().all(|f| { + matches!(f, ObjectField::KeyValue { value, .. } if matches!(value.kind, ExprKind::List(_))) + }) +} From e5b41e6dfe98c942bd138226ddb1802c2620739c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:24:17 +0200 Subject: [PATCH 077/106] RHLGA-1: FMTSPEC-1/FORMATFN-1 one Rust std::fmt engine for println/print/format - src/runtime/fmt_spec.rs: one parser for format strings and specs: {} {0} {name} {{ }}, fill/align, +, #, 0, width, .precision, ? x X b o e E; width/precision from an argument ({:1$}, {:.*}) is an error naming the spec - interpreter_format_calls.rs: println/print/eprintln/eprint/format (function and macro forms) format through it; {name} resolves named args, then scope - format("{:?}", [1, 2]) no longer fails with Unknown builtin __builtin_format__ - f-string {x:.2} specs use the same parser - legacy tests that enshrined silent placeholder text now expect the error - property tests compare int/float/string output with Rust's own format! Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 164 ++--- src/runtime/eval_builtin.rs | 97 +-- src/runtime/eval_builtin_prop_tests_part1.rs | 2 +- src/runtime/fmt_spec.rs | 638 ++++++++++++++++++ src/runtime/fmt_spec_tests.rs | 193 ++++++ src/runtime/interpreter.rs | 7 +- src/runtime/interpreter_core_tests_part1.rs | 4 +- src/runtime/interpreter_core_tests_part2.rs | 19 +- src/runtime/interpreter_format_calls.rs | 196 ++++++ src/runtime/interpreter_misc_eval.rs | 74 +- .../interpreter_tests_part10.rs | 3 +- .../interpreter_tests_part6.rs | 10 +- .../interpreter_tests_part9.rs | 2 +- src/runtime/mod.rs | 2 + src/runtime/value_format.rs | 33 +- 15 files changed, 1186 insertions(+), 258 deletions(-) create mode 100644 src/runtime/fmt_spec.rs create mode 100644 src/runtime/fmt_spec_tests.rs create mode 100644 src/runtime/interpreter_format_calls.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 9d4810717..586c6d716 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T09:27:46.800038503Z", + "created_at": "2026-09-24T09:27:48.275323465Z", "git_context": null, "files": { "./build.rs": { @@ -5317,48 +5317,48 @@ }, "./src/backend/transpiler/expressions.rs": { "content_hash": [ - 160, - 132, - 90, - 92, - 85, - 75, - 214, - 25, - 109, - 223, + 21, + 152, 118, - 69, - 62, - 186, - 85, - 114, - 175, - 138, - 164, - 142, - 145, + 200, + 239, + 102, + 24, + 150, + 191, + 8, + 180, + 191, + 32, + 232, + 92, + 32, + 201, 33, - 104, - 203, - 70, - 213, - 36, - 154, - 204, + 178, + 186, + 162, + 88, + 69, + 212, 79, - 77, - 132 + 153, + 112, + 205, + 179, + 6, + 6, + 87 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 13.222632, + "duplication_ratio": 13.293051, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 73.22263, + "total": 73.29305, "grade": "B-", "confidence": 1.0, "language": "Rust", @@ -5370,15 +5370,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 129 duplicate code patterns" + "issue": "Found 131 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 6.7773676, + "amount": 6.7069488, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 33.9%" + "issue": "Code duplication: 33.5%" }, { "source_metric": "StructuralComplexity", @@ -5386,7 +5386,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 49" + "issue": "High cognitive complexity: 53" }, { "source_metric": "StructuralComplexity", @@ -5394,7 +5394,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 117" + "issue": "High cyclomatic complexity: 120" } ], "critical_defects_count": 0, @@ -5665,48 +5665,48 @@ }, "./src/backend/transpiler/expressions_helpers/collections.rs": { "content_hash": [ - 49, - 36, - 177, - 54, - 138, - 196, - 222, - 226, - 36, - 24, - 246, - 28, - 246, - 185, - 104, - 114, - 233, - 118, - 160, - 157, - 157, - 162, - 87, - 52, - 227, - 188, - 238, - 84, - 31, - 15, - 1, - 43 + 113, + 11, + 143, + 173, + 192, + 59, + 158, + 241, + 143, + 124, + 92, + 47, + 215, + 112, + 42, + 112, + 123, + 169, + 189, + 171, + 88, + 11, + 204, + 181, + 94, + 40, + 100, + 179, + 85, + 189, + 141, + 194 ], "score": { - "structural_complexity": 16.599998, + "structural_complexity": 14.599999, "semantic_complexity": 20.0, - "duplication_ratio": 12.714682, + "duplication_ratio": 12.894737, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 89.31468, + "total": 87.494736, "grade": "A-", "confidence": 1.0, "language": "Rust", @@ -5718,23 +5718,31 @@ "applied_to": [ "Duplication" ], - "issue": "Found 72 duplicate code patterns" + "issue": "Found 77 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 7.2853184, + "amount": 7.1052628, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 36.4%" + "issue": "Code duplication: 35.5%" }, { "source_metric": "StructuralComplexity", - "amount": 8.400001, + "amount": 9.900001, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 43" + "issue": "High cognitive complexity: 48" + }, + { + "source_metric": "StructuralComplexity", + "amount": 0.5, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 31" } ], "critical_defects_count": 0, @@ -45054,7 +45062,7 @@ }, "summary": { "total_files": 530, - "avg_score": 90.27595, + "avg_score": 90.27265, "grade_distribution": { "A+": 195, "A": 174, diff --git a/src/runtime/eval_builtin.rs b/src/runtime/eval_builtin.rs index 80af7331c..ceb2ab002 100644 --- a/src/runtime/eval_builtin.rs +++ b/src/runtime/eval_builtin.rs @@ -11,11 +11,11 @@ //! - `eval_builtin_json_ops`: JSON operations (parse, stringify, merge, get, set) //! - `eval_builtin_platform`: HTTP, HTML, Process, File, String, and type conversions +use crate::runtime::interpreter_format_calls::{eval_builtin_format, FormatSink}; use crate::runtime::validation::validate_arg_count; use crate::runtime::{InterpreterError, Value}; use std::collections::HashMap; -use std::io::Write; use std::sync::Arc; // Re-export from sub-modules so that `use super::*` in test modules still works @@ -78,7 +78,11 @@ fn try_eval_io_function(name: &str, args: &[Value]) -> Result, Int "__builtin_println__" => Ok(Some(eval_println(args)?)), "__builtin_print__" => Ok(Some(eval_print(args)?)), "__builtin_dbg__" => Ok(Some(eval_dbg(args)?)), - _ => Ok(None), + // FMTSPEC-1 / EPRINTLN-1 / FORMATFN-1: one format engine for all five + _ => match FormatSink::for_marker(name) { + Some(sink) => Ok(Some(eval_builtin_format(sink, args)?)), + None => Ok(None), + }, } } @@ -282,28 +286,25 @@ fn try_eval_dataframe_function( } } -/// Print values to stdout with newline -/// -/// Supports printf-style formatting with {} placeholders: -/// - `println("Count: {}", 42)` → "Count: 42" -/// - `println("Name: {}, Age: {}", "Alice", 30)` → "Name: Alice, Age: 30" -/// -/// Format value for println (strings without quotes) -/// Complexity: 2 (within Toyota Way limits) -fn format_value_for_println(value: &Value) -> String { - match value { - Value::String(s) => s.to_string(), - other => format!("{other}"), - } +/// Print values to stdout with newline. FMTSPEC-1: the first value is a +/// Rust format string when more values follow it (`println("{:>5}", 7)`). +fn eval_println(args: &[Value]) -> Result { + eval_builtin_format(FormatSink::Println, args) } -/// Format string with interpolation: `{}` raw, `{:?}` Rust Debug -fn format_with_interpolation(fmt_str: &str, args: &[Value]) -> String { - crate::runtime::value_format::format_string_with_values(fmt_str, args) +/// Print values to stdout without newline, formatted like [`eval_println`]. +fn eval_print(args: &[Value]) -> Result { + eval_builtin_format(FormatSink::Print, args) } -/// Join values with spaces -/// Complexity: 1 (within Toyota Way limits) +/// Text of one println argument (strings without quotes); unit-test API. +#[cfg(test)] +fn format_value_for_println(value: &Value) -> String { + crate::runtime::value_format::format_value_display(value) +} + +/// Values joined by spaces, as `println(a, b)` prints them; unit-test API. +#[cfg(test)] fn join_values(args: &[Value]) -> String { args.iter() .map(format_value_for_println) @@ -311,59 +312,11 @@ fn join_values(args: &[Value]) -> String { .join(" ") } -/// Format println output -/// Complexity: 3 (within Toyota Way limits, reduced from 7) +/// The line `println(args)` prints (an error prints its message); unit-test API. +#[cfg(test)] fn format_println_output(args: &[Value]) -> String { - if args.is_empty() { - "\n".to_string() - } else if let Value::String(fmt_str) = &args[0] { - // RHLGA-1: the function form substitutes {} and {:?} like println!() - if fmt_str.contains("{}") || fmt_str.contains("{:?}") { - format!("{}\n", format_with_interpolation(fmt_str, &args[1..])) - } else { - format!("{}\n", join_values(args)) - } - } else { - format!("{}\n", join_values(args)) - } -} - -/// Print values to stdout with newline -/// Complexity: 2 (within Toyota Way limits, reduced from 7) -fn eval_println(args: &[Value]) -> Result { - let output = format_println_output(args); - - // Write to output buffer (for notebook capture) - if let Ok(mut buf) = crate::runtime::builtins::OUTPUT_BUFFER.lock() { - buf.push_str(&output); - } - - // Also write to stdout for local REPL use - print!("{output}"); - let _ = std::io::stdout().flush(); - - Ok(Value::Nil) -} - -/// Print values to stdout without newline -/// -fn eval_print(args: &[Value]) -> Result { - let output = args - .iter() - .map(|v| format!("{v}")) - .collect::>() - .join(" "); - - // Write to output buffer (for notebook capture) - if let Ok(mut buf) = crate::runtime::builtins::OUTPUT_BUFFER.lock() { - buf.push_str(&output); - } - - // Also write to stdout for local REPL use - print!("{output}"); - let _ = std::io::stdout().flush(); - - Ok(Value::Nil) + crate::runtime::fmt_spec::format_call(args, args.len() > 1, &|_| None) + .map_or_else(|e| e, |text| format!("{text}\n")) } /// Debug print with value inspection diff --git a/src/runtime/eval_builtin_prop_tests_part1.rs b/src/runtime/eval_builtin_prop_tests_part1.rs index 4610b42af..df2a3b875 100644 --- a/src/runtime/eval_builtin_prop_tests_part1.rs +++ b/src/runtime/eval_builtin_prop_tests_part1.rs @@ -1848,7 +1848,7 @@ fn test_format_value_for_println_integer() { #[test] fn test_format_with_interpolation_multiple() { - let result = format_with_interpolation( + let result = crate::runtime::value_format::format_string_with_values( "{} + {} = {}", &[Value::Integer(1), Value::Integer(2), Value::Integer(3)], ); diff --git a/src/runtime/fmt_spec.rs b/src/runtime/fmt_spec.rs new file mode 100644 index 000000000..43343871e --- /dev/null +++ b/src/runtime/fmt_spec.rs @@ -0,0 +1,638 @@ +//! FMTSPEC-1: Rust `std::fmt` format strings for the interpreter. +//! +//! One parser for the format-spec language, shared by `println`/`print`/ +//! `eprintln`/`eprint`/`format` (function and macro forms) and by the +//! `{expr:spec}` f-string interpolation. +//! +//! Supported: `{}`, `{0}`, `{name}`, `{{`/`}}`, fill/align `<^>`, `+`, `#`, +//! `0`, width, `.precision`, and the traits `?`, `x`, `X`, `b`, `o`, `e`, `E`. +//! Width or precision taken from an argument (`{:1$}`, `{:.*}`, `{:w$}`) is +//! an error naming the spec, never silently wrong output. + +use crate::runtime::value_format::{format_value_display, rust_debug}; +use crate::runtime::Value; +use std::collections::HashMap; + +/// Which argument a field refers to. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ArgRef { + /// `{}`: the next implicit positional argument. + Next, + /// `{0}`: an explicit positional argument. + Index(usize), + /// `{name}`: a named argument or a variable in scope. + Name(String), +} + +/// Alignment inside the field width. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Align { + Left, + Center, + Right, +} + +/// The formatting trait a field selects. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + Display, + Debug, + LowerHex, + UpperHex, + Binary, + Octal, + LowerExp, + UpperExp, +} + +/// A parsed `:spec`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Spec { + pub fill: char, + pub align: Option, + pub plus: bool, + pub alternate: bool, + pub zero: bool, + pub width: Option, + pub precision: Option, + pub kind: Kind, +} + +impl Default for Spec { + fn default() -> Self { + Self { + fill: ' ', + align: None, + plus: false, + alternate: false, + zero: false, + width: None, + precision: None, + kind: Kind::Display, + } + } +} + +/// One piece of a parsed format string. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Piece { + Text(String), + Field(ArgRef, Spec), +} + +type Chars<'a> = std::iter::Peekable>; + +/// Parse a format string into text and fields. +pub fn parse(fmt: &str) -> Result, String> { + let mut pieces = Vec::new(); + let mut text = String::new(); + let mut chars = fmt.chars().peekable(); + while let Some(c) = chars.next() { + parse_char(c, &mut chars, &mut pieces, &mut text)?; + } + flush_text(&mut pieces, &mut text); + Ok(pieces) +} + +/// One character of a format string: a `{{`/`}}` escape, a field, or text. +fn parse_char( + c: char, + chars: &mut Chars<'_>, + pieces: &mut Vec, + text: &mut String, +) -> Result<(), String> { + if matches!(c, '{' | '}') && chars.peek() == Some(&c) { + chars.next(); + text.push(c); + return Ok(()); + } + match c { + '{' => { + flush_text(pieces, text); + pieces.push(parse_field(&take_field(chars)?)?); + Ok(()) + } + '}' => Err("unmatched `}` in format string (use `}}` for a literal brace)".into()), + _ => { + text.push(c); + Ok(()) + } + } +} + +fn flush_text(pieces: &mut Vec, text: &mut String) { + if !text.is_empty() { + pieces.push(Piece::Text(std::mem::take(text))); + } +} + +fn take_field(chars: &mut Chars<'_>) -> Result { + let mut body = String::new(); + for c in chars.by_ref() { + match c { + '}' => return Ok(body), + '{' => return Err(format!("invalid format field `{{{body}{{`")), + _ => body.push(c), + } + } + Err(format!( + "unterminated format field `{{{body}` (use `{{{{` for a literal brace)" + )) +} + +fn parse_field(body: &str) -> Result { + let (arg, spec) = match body.split_once(':') { + Some((arg, spec)) => (arg, Some(spec)), + None => (body, None), + }; + let spec = match spec { + Some(text) => parse_spec(text)?, + None => Spec::default(), + }; + Ok(Piece::Field(parse_arg(arg.trim())?, spec)) +} + +fn parse_arg(arg: &str) -> Result { + if arg.is_empty() { + return Ok(ArgRef::Next); + } + if let Ok(index) = arg.parse::() { + return Ok(ArgRef::Index(index)); + } + if is_identifier(arg) { + return Ok(ArgRef::Name(arg.to_string())); + } + Err(format!("invalid format argument `{arg}`")) +} + +fn is_identifier(s: &str) -> bool { + let mut chars = s.chars(); + let first_ok = chars.next().is_some_and(|c| c.is_alphabetic() || c == '_'); + first_ok && chars.all(|c| c.is_alphanumeric() || c == '_') +} + +/// Parse the text after `:` in a field. +pub fn parse_spec(text: &str) -> Result { + parse_spec_chars(&text.chars().collect::>()) + .map_err(|e| format!("{e} in format spec `{{:{text}}}`")) +} + +fn parse_spec_chars(chars: &[char]) -> Result { + let mut spec = Spec::default(); + let mut i = parse_fill_align(chars, &mut spec); + i = parse_flags(chars, i, &mut spec); + let (width, mut i) = parse_count(chars, i)?; + spec.width = width; + if chars.get(i) == Some(&'.') { + let (precision, next) = parse_precision(chars, i + 1)?; + spec.precision = Some(precision); + i = next; + } + spec.kind = parse_kind(&chars[i..].iter().collect::())?; + Ok(spec) +} + +fn align_of(c: char) -> Option { + match c { + '<' => Some(Align::Left), + '^' => Some(Align::Center), + '>' => Some(Align::Right), + _ => None, + } +} + +fn parse_fill_align(chars: &[char], spec: &mut Spec) -> usize { + if let Some(align) = chars.get(1).copied().and_then(align_of) { + spec.fill = chars[0]; + spec.align = Some(align); + return 2; + } + if let Some(align) = chars.first().copied().and_then(align_of) { + spec.align = Some(align); + return 1; + } + 0 +} + +fn parse_flags(chars: &[char], mut i: usize, spec: &mut Spec) -> usize { + if matches!(chars.get(i), Some('+' | '-')) { + spec.plus = chars[i] == '+'; + i += 1; + } + if chars.get(i) == Some(&'#') { + spec.alternate = true; + i += 1; + } + if chars.get(i) == Some(&'0') { + spec.zero = true; + i += 1; + } + i +} + +/// Digits at `i`; a count taken from an argument (`1$`) is refused. +fn parse_count(chars: &[char], i: usize) -> Result<(Option, usize), String> { + let end = i + chars[i..].iter().take_while(|c| c.is_ascii_digit()).count(); + if chars.get(end) == Some(&'$') { + return Err("width/precision taken from an argument (`N$`) is not supported".into()); + } + let digits: String = chars[i..end].iter().collect(); + Ok((digits.parse().ok(), end)) +} + +fn parse_precision(chars: &[char], i: usize) -> Result<(usize, usize), String> { + if chars.get(i) == Some(&'*') { + return Err("precision taken from an argument (`.*`) is not supported".into()); + } + match parse_count(chars, i)? { + (Some(precision), next) => Ok((precision, next)), + (None, _) => Err("missing precision after `.`".into()), + } +} + +fn parse_kind(text: &str) -> Result { + match text { + "" => Ok(Kind::Display), + "?" => Ok(Kind::Debug), + "x" => Ok(Kind::LowerHex), + "X" => Ok(Kind::UpperHex), + "b" => Ok(Kind::Binary), + "o" => Ok(Kind::Octal), + "e" => Ok(Kind::LowerExp), + "E" => Ok(Kind::UpperExp), + other => Err(format!("unsupported format trait `{other}`")), + } +} + +/// Render parsed pieces. `named` resolves `{name}` fields. +pub fn render( + pieces: &[Piece], + args: &[Value], + named: &dyn Fn(&str) -> Option, +) -> Result { + let mut out = String::new(); + let mut next = 0; + for piece in pieces { + match piece { + Piece::Text(text) => out.push_str(text), + Piece::Field(arg, spec) => { + let value = resolve(arg, args, &mut next, named)?; + out.push_str(&format_value(&value, spec)?); + } + } + } + Ok(out) +} + +fn resolve( + arg: &ArgRef, + args: &[Value], + next: &mut usize, + named: &dyn Fn(&str) -> Option, +) -> Result { + match arg { + ArgRef::Next => { + *next += 1; + args.get(*next - 1).cloned().ok_or_else(|| { + format!( + "format string needs {} positional argument(s), {} given", + *next, + args.len() + ) + }) + } + ArgRef::Index(i) => args.get(*i).cloned().ok_or_else(|| { + format!( + "invalid reference to positional argument {i} ({} given)", + args.len() + ) + }), + ArgRef::Name(name) => { + named(name).ok_or_else(|| format!("there is no argument named `{name}`")) + } + } +} + +/// Parse and render `fmt` in one step. +pub fn format_str( + fmt: &str, + args: &[Value], + named: &dyn Fn(&str) -> Option, +) -> Result { + render(&parse(fmt)?, args, named) +} + +/// Text of an output call (`println(args…)`, `format(args…)`). +/// +/// With `template`, the first value is the format string. A template with no +/// fields and extra arguments keeps the historical ruchy form +/// (`println("a", 1)` prints `a 1`), which Rust rejects at compile time. +/// Without a template the values are joined by spaces. +pub fn format_call( + values: &[Value], + template: bool, + named: &dyn Fn(&str) -> Option, +) -> Result { + let Some((Value::String(fmt), rest)) = values.split_first() else { + return Ok(join_display(values)); + }; + if !template { + return Ok(join_display(values)); + } + let pieces = parse(fmt)?; + let has_field = pieces.iter().any(|p| matches!(p, Piece::Field(..))); + if !has_field && !rest.is_empty() { + return Ok(join_display(values)); + } + render(&pieces, rest, named) +} + +fn join_display(values: &[Value]) -> String { + values + .iter() + .map(format_value_display) + .collect::>() + .join(" ") +} + +/// Format one value under a spec. +pub fn format_value(value: &Value, spec: &Spec) -> Result { + if spec.kind == Kind::Debug && is_compound(value) { + return debug_compound(value, spec); + } + format_scalar(value, spec) +} + +fn is_compound(value: &Value) -> bool { + matches!( + value, + Value::Array(_) + | Value::Tuple(_) + | Value::Struct { .. } + | Value::Object(_) + | Value::ObjectMut(_) + | Value::EnumVariant { .. } + ) +} + +fn format_scalar(value: &Value, spec: &Spec) -> Result { + let (sign, body, numeric) = match value { + Value::Integer(i) => (int_sign(*i, spec), int_digits(*i, spec), true), + Value::Float(f) => (float_sign(*f, spec), float_digits(*f, spec)?, true), + // `impl Debug for str` writes the quoted text and ignores width. + Value::String(_) if spec.kind == Kind::Debug => return Ok(rust_debug(value)), + _ => (String::new(), text_body(value, spec)?, false), + }; + Ok(pad(&sign, &body, spec, numeric)) +} + +fn is_radix(kind: Kind) -> bool { + matches!( + kind, + Kind::LowerHex | Kind::UpperHex | Kind::Binary | Kind::Octal + ) +} + +fn int_sign(i: i64, spec: &Spec) -> String { + let radix = is_radix(spec.kind); + let sign = if i < 0 && !radix { + "-" + } else if spec.plus { + "+" + } else { + "" + }; + let prefix = match spec.kind { + Kind::LowerHex | Kind::UpperHex if spec.alternate => "0x", + Kind::Binary if spec.alternate => "0b", + Kind::Octal if spec.alternate => "0o", + _ => "", + }; + format!("{sign}{prefix}") +} + +fn int_digits(i: i64, spec: &Spec) -> String { + let magnitude = i.unsigned_abs(); + match (spec.kind, spec.precision) { + (Kind::LowerHex, _) => format!("{i:x}"), + (Kind::UpperHex, _) => format!("{i:X}"), + (Kind::Binary, _) => format!("{i:b}"), + (Kind::Octal, _) => format!("{i:o}"), + (Kind::LowerExp, Some(p)) => format!("{magnitude:.p$e}"), + (Kind::LowerExp, None) => format!("{magnitude:e}"), + (Kind::UpperExp, Some(p)) => format!("{magnitude:.p$E}"), + (Kind::UpperExp, None) => format!("{magnitude:E}"), + (Kind::Display | Kind::Debug, _) => magnitude.to_string(), + } +} + +fn float_sign(f: f64, spec: &Spec) -> String { + let sign = if f.is_sign_negative() && !f.is_nan() { + "-" + } else if spec.plus { + "+" + } else { + "" + }; + sign.to_string() +} + +fn float_digits(f: f64, spec: &Spec) -> Result { + let a = f.abs(); + Ok(match (spec.kind, spec.precision) { + (Kind::LowerExp, Some(p)) => format!("{a:.p$e}"), + (Kind::LowerExp, None) => format!("{a:e}"), + (Kind::UpperExp, Some(p)) => format!("{a:.p$E}"), + (Kind::UpperExp, None) => format!("{a:E}"), + (Kind::Display | Kind::Debug, Some(p)) => format!("{a:.p$}"), + (Kind::Display, None) => format_value_display(&Value::Float(a)), + (Kind::Debug, None) => rust_debug(&Value::Float(a)), + (kind, _) => { + return Err(format!( + "format trait {kind:?} needs an integer, got a float" + )) + } + }) +} + +fn text_body(value: &Value, spec: &Spec) -> Result { + match spec.kind { + // Rust's `pad` truncates str and bool; other Display impls ignore precision. + Kind::Display if matches!(value, Value::String(_) | Value::Bool(_)) => { + Ok(truncate(&format_value_display(value), spec.precision)) + } + Kind::Display => Ok(format_value_display(value)), + Kind::Debug => Ok(rust_debug(value)), + kind => Err(format!( + "format trait {kind:?} needs a number, got {}", + value.type_name() + )), + } +} + +fn truncate(text: &str, precision: Option) -> String { + match precision { + Some(p) => text.chars().take(p).collect(), + None => text.to_string(), + } +} + +/// Apply width, fill and alignment. `0` pads numbers after the sign. +fn pad(sign: &str, body: &str, spec: &Spec, numeric: bool) -> String { + let len = sign.chars().count() + body.chars().count(); + let gap = spec.width.unwrap_or(0).saturating_sub(len); + if spec.zero && numeric { + return format!("{sign}{}{body}", "0".repeat(gap)); + } + let default = if numeric { Align::Right } else { Align::Left }; + let (left, right) = match spec.align.unwrap_or(default) { + Align::Left => (0, gap), + Align::Right => (gap, 0), + Align::Center => (gap / 2, gap - gap / 2), + }; + let fill = |n: usize| std::iter::repeat_n(spec.fill, n).collect::(); + format!("{}{sign}{body}{}", fill(left), fill(right)) +} + +// ------------------------------------------------------------- compound `{:?}` + +struct Delims<'a> { + open: &'a str, + close: &'a str, + spaced: bool, + one_tuple: bool, +} + +const LIST: Delims<'static> = Delims { + open: "[", + close: "]", + spaced: false, + one_tuple: false, +}; + +/// Rust's derived/std Debug for a compound value: the spec reaches every +/// leaf (`{:5?}` pads each element) and `#` selects the pretty form. +fn debug_compound(value: &Value, spec: &Spec) -> Result { + match value { + Value::Array(items) => debug_seq(&LIST, items, spec), + Value::Tuple(items) => debug_seq(&tuple_delims(items.len()), items, spec), + Value::EnumVariant { + variant_name, data, .. + } => debug_variant(variant_name, data.as_deref().unwrap_or(&[]), spec), + Value::Struct { name, fields } => debug_map(Some(name), fields, spec), + Value::Object(map) => debug_object(map, spec), + Value::ObjectMut(cell) => { + let map = cell + .lock() + .map_err(|_| "object lock poisoned".to_string())?; + debug_object(&map, spec) + } + _ => format_scalar(value, spec), + } +} + +fn tuple_delims(len: usize) -> Delims<'static> { + Delims { + open: "(", + close: ")", + spaced: false, + one_tuple: len == 1, + } +} + +fn debug_seq(delims: &Delims<'_>, items: &[Value], spec: &Spec) -> Result { + let parts = items + .iter() + .map(|v| format_value(v, spec)) + .collect::, _>>()?; + Ok(wrap(delims, &parts, spec.alternate)) +} + +fn debug_variant(name: &str, data: &[Value], spec: &Spec) -> Result { + if data.is_empty() { + return Ok(name.to_string()); + } + let open = format!("{name}("); + let delims = Delims { + open: &open, + close: ")", + spaced: false, + one_tuple: false, + }; + debug_seq(&delims, data, spec) +} + +fn debug_object(map: &HashMap, spec: &Spec) -> Result { + match map.get("__class") { + Some(Value::String(class)) => debug_map(Some(class), map, spec), + _ => debug_map(None, map, spec), + } +} + +/// A struct (`Name { a: 1 }`) or, without a name, a map (`{"a": 1}`). +fn debug_map( + name: Option<&str>, + map: &HashMap, + spec: &Spec, +) -> Result { + let mut entries: Vec<_> = map.iter().filter(|(k, _)| !k.starts_with("__")).collect(); + entries.sort_by(|a, b| a.0.cmp(b.0)); + let parts = entries + .into_iter() + .map(|(k, v)| { + let key = if name.is_some() { + k.clone() + } else { + format!("{k:?}") + }; + format_value(v, spec).map(|v| format!("{key}: {v}")) + }) + .collect::, _>>()?; + match name { + Some(name) if parts.is_empty() => Ok(name.to_string()), + Some(name) => { + let open = format!("{name} {{"); + let delims = Delims { + open: &open, + close: "}", + spaced: true, + one_tuple: false, + }; + Ok(wrap(&delims, &parts, spec.alternate)) + } + None => { + let delims = Delims { + open: "{", + close: "}", + spaced: false, + one_tuple: false, + }; + Ok(wrap(&delims, &parts, spec.alternate)) + } + } +} + +fn wrap(delims: &Delims<'_>, parts: &[String], pretty: bool) -> String { + let (open, close) = (delims.open, delims.close); + if parts.is_empty() { + return format!("{open}{close}"); + } + if pretty { + let body: String = parts.iter().map(|p| format!("{},\n", indent(p))).collect(); + return format!("{open}\n{body}{close}"); + } + let space = if delims.spaced { " " } else { "" }; + let trail = if delims.one_tuple { "," } else { "" }; + format!("{open}{space}{}{trail}{space}{close}", parts.join(", ")) +} + +fn indent(text: &str) -> String { + text.lines() + .map(|line| format!(" {line}")) + .collect::>() + .join("\n") +} + +#[cfg(test)] +#[path = "fmt_spec_tests.rs"] +mod tests; diff --git a/src/runtime/fmt_spec_tests.rs b/src/runtime/fmt_spec_tests.rs new file mode 100644 index 000000000..ab0926186 --- /dev/null +++ b/src/runtime/fmt_spec_tests.rs @@ -0,0 +1,193 @@ +//! FMTSPEC-1 unit and property tests: every expected string is computed by +//! Rust's own `format!` in the test, so the oracle is std::fmt itself. + +use super::*; +use proptest::prelude::*; + +fn fmt(template: &str, args: &[Value]) -> Result { + format_str(template, args, &|_| None) +} + +fn s(text: &str) -> Value { + Value::from_string(text.to_string()) +} + +#[test] +fn test_fmtspec_1_brief_case() { + let got = fmt( + "[{:>5}] [{:.2}] [{0}]", + &[Value::Integer(7), Value::Float(3.14159)], + ); + assert_eq!(got.unwrap(), format!("[{:>5}] [{:.2}] [{0}]", 7, 3.14159)); +} + +#[test] +fn test_fmtspec_1_escapes() { + assert_eq!(fmt("{{}} {{{}}}", &[Value::Integer(1)]).unwrap(), "{} {1}"); +} + +#[test] +fn test_fmtspec_1_named_resolver() { + let got = format_str("{a}-{b:>3}", &[], &|n| match n { + "a" => Some(Value::Integer(1)), + "b" => Some(s("z")), + _ => None, + }); + assert_eq!(got.unwrap(), format!("{}-{:>3}", 1, "z")); + let missing = format_str("{nope}", &[], &|_| None).unwrap_err(); + assert!(missing.contains("nope"), "{missing}"); +} + +#[test] +fn test_fmtspec_1_radix_sign_alternate_zero() { + let args: Vec = [255, 255, 5, 8, 255, 5, 8, 5, 255, -1, 255] + .iter() + .map(|i| Value::Integer(*i)) + .collect(); + let got = fmt( + "{:x} {:X} {:b} {:o} {:#x} {:#b} {:#o} {:08b} {:#06x} {:x} {:+X}", + &args, + ); + let want = format!( + "{:x} {:X} {:b} {:o} {:#x} {:#b} {:#o} {:08b} {:#06x} {:x} {:+X}", + 255, 255, 5, 8, 255, 5, 8, 5, 255, -1i64, 255 + ); + assert_eq!(got.unwrap(), want); +} + +#[test] +fn test_fmtspec_1_exp_forms() { + let args = [ + Value::Float(1234.5), + Value::Float(0.00012), + Value::Integer(1234), + Value::Float(1234.5), + Value::Float(1.5), + ]; + let got = fmt("{:e} {:E} {:e} {:>10.2e} {:+e}", &args); + let want = format!( + "{:e} {:E} {:e} {:>10.2e} {:+e}", + 1234.5, 0.00012, 1234i64, 1234.5, 1.5 + ); + assert_eq!(got.unwrap(), want); +} + +#[test] +fn test_fmtspec_1_debug_forms() { + let arr = Value::from_array(vec![Value::Integer(1), Value::Integer(2)]); + let got = fmt( + "{:?}|{:5?}|{:>6?}|{:#?}|{:08.2?}", + &[arr.clone(), arr.clone(), s("ab"), arr, Value::Float(-1.5)], + ); + let v = vec![1, 2]; + let want = format!("{:?}|{:5?}|{:>6?}|{:#?}|{:08.2?}", v, v, "ab", v, -1.5); + assert_eq!(got.unwrap(), want); +} + +#[test] +fn test_fmtspec_1_pretty_tuple_and_variant() { + let tuple = Value::Tuple(vec![Value::Integer(1)].into()); + let some = Value::EnumVariant { + enum_name: "Option".to_string(), + variant_name: "Some".to_string(), + data: Some(vec![Value::Integer(3)]), + }; + let got = fmt("{:#?}|{:?}|{:#?}", &[tuple.clone(), tuple, some]); + assert_eq!( + got.unwrap(), + format!("{:#?}|{:?}|{:#?}", (1,), (1,), Some(3)) + ); +} + +#[test] +fn test_fmtspec_1_argument_counts_are_refused() { + for bad in ["{:1$}", "{:.*}", "{:w$}", "{:.1$}"] { + let err = fmt(bad, &[Value::Integer(1), Value::Integer(2)]).unwrap_err(); + assert!(err.contains("$") || err.contains(".*"), "{bad}: {err}"); + } +} + +#[test] +fn test_fmtspec_1_malformed_strings_are_errors() { + for bad in ["{", "}", "{:q}", "{0", "{a b}", "{:.}"] { + assert!(fmt(bad, &[Value::Integer(1)]).is_err(), "{bad}"); + } + assert!(fmt("{} {}", &[Value::Integer(1)]).is_err()); + assert!(fmt("{3}", &[Value::Integer(1)]).is_err()); + assert!(fmt("{:x}", &[Value::Float(1.0)]).is_err()); + assert!(fmt("{:x}", &[s("a")]).is_err()); +} + +#[test] +fn test_fmtspec_1_format_call_legacy_join() { + let named = |_: &str| None; + let joined = format_call(&[s("a"), Value::Integer(1)], true, &named); + assert_eq!(joined.unwrap(), "a 1"); + assert_eq!(format_call(&[s("x{{")], false, &named).unwrap(), "x{{"); + assert_eq!(format_call(&[s("x{{")], true, &named).unwrap(), "x{"); + assert_eq!(format_call(&[], true, &named).unwrap(), ""); +} + +#[test] +fn test_fmtspec_1_struct_and_map_debug() { + let mut fields = HashMap::new(); + fields.insert("a".to_string(), Value::Integer(1)); + let st = Value::Struct { + name: "P".to_string(), + fields: std::sync::Arc::new(fields.clone()), + }; + let map = Value::Object(std::sync::Arc::new(fields)); + let got = fmt("{:?}|{:#?}|{:?}|{:#?}", &[st.clone(), st, map.clone(), map]); + #[derive(Debug)] + #[allow(dead_code)] + struct P { + a: i64, + } + let m: std::collections::BTreeMap<&str, i64> = [("a", 1)].into_iter().collect(); + let want = format!("{:?}|{:#?}|{:?}|{:#?}", P { a: 1 }, P { a: 1 }, m, m); + assert_eq!(got.unwrap(), want); +} + +/// Rust's own output for a spec prefix chosen by index, with runtime width +/// and precision. +macro_rules! rust_ref { + ($idx:expr, $v:expr, $w:expr, $p:expr) => { + match $idx { + 0 => format!("{:w$.p$}", $v, w = $w, p = $p), + 1 => format!("{: format!("{:^w$.p$}", $v, w = $w, p = $p), + 3 => format!("{:>w$.p$}", $v, w = $w, p = $p), + 4 => format!("{:* format!("{:*^w$.p$}", $v, w = $w, p = $p), + 6 => format!("{:*>w$.p$}", $v, w = $w, p = $p), + 7 => format!("{:0w$.p$}", $v, w = $w, p = $p), + _ => format!("{:+w$.p$}", $v, w = $w, p = $p), + } + }; +} + +const PREFIXES: [&str; 9] = ["", "<", "^", ">", "*<", "*^", "*>", "0", "+"]; + +fn ours(idx: usize, value: &Value, w: usize, p: usize) -> String { + let spec = format!("{}{w}.{p}", PREFIXES[idx]); + fmt(&format!("{{:{spec}}}"), std::slice::from_ref(value)).expect("valid spec") +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(512))] + + #[test] + fn prop_fmtspec_1_ints_match_rust(idx in 0usize..9, v in -100_000i64..100_000, w in 1usize..14, p in 0usize..6) { + prop_assert_eq!(ours(idx, &Value::Integer(v), w, p), rust_ref!(idx, v, w, p)); + } + + #[test] + fn prop_fmtspec_1_floats_match_rust(idx in 0usize..9, v in -1.0e5f64..1.0e5, w in 1usize..14, p in 0usize..6) { + prop_assert_eq!(ours(idx, &Value::Float(v), w, p), rust_ref!(idx, v, w, p)); + } + + #[test] + fn prop_fmtspec_1_strings_match_rust(idx in 0usize..9, v in "[a-z]{0,8}", w in 1usize..14, p in 0usize..10) { + prop_assert_eq!(ours(idx, &s(&v), w, p), rust_ref!(idx, v.as_str(), w, p)); + } +} diff --git a/src/runtime/interpreter.rs b/src/runtime/interpreter.rs index 7521af407..09aaaf012 100644 --- a/src/runtime/interpreter.rs +++ b/src/runtime/interpreter.rs @@ -277,7 +277,7 @@ impl Interpreter { match expr_kind { ExprKind::Binary { left, op, right } => self.eval_binary_expr(left, *op, right), ExprKind::Unary { op, operand } => self.eval_unary_expr(*op, operand), - ExprKind::Call { func, args } => self.eval_function_call(func, args), + ExprKind::Call { func, args } => self.eval_call_expr(func, args), ExprKind::MethodCall { receiver, method, @@ -373,11 +373,6 @@ impl Interpreter { Some(current_value) } - // Value formatting delegated to value_format module - // EXTREME TDD: Eliminated 50 lines of duplicate code - pub(crate) fn format_string_with_values(format_str: &str, values: &[Value]) -> String { - crate::runtime::value_format::format_string_with_values(format_str, values) - } /// Evaluate miscellaneous expressions. /// Delegates to `interpreter_misc_eval` module. pub(crate) fn eval_misc_expr( diff --git a/src/runtime/interpreter_core_tests_part1.rs b/src/runtime/interpreter_core_tests_part1.rs index db26f6453..f9991d97e 100644 --- a/src/runtime/interpreter_core_tests_part1.rs +++ b/src/runtime/interpreter_core_tests_part1.rs @@ -381,7 +381,7 @@ fn test_env_set_mut_updates_parent() { // Test format_string_with_values #[test] fn test_format_string_with_values() { - let result = Interpreter::format_string_with_values( + let result = crate::runtime::value_format::format_string_with_values( "Hello {} and {}!", &[Value::from_string("world".to_string()), Value::Integer(42)], ); @@ -390,7 +390,7 @@ fn test_format_string_with_values() { #[test] fn test_format_string_with_values_fewer_args() { - let result = Interpreter::format_string_with_values( + let result = crate::runtime::value_format::format_string_with_values( "Hello {} and {}!", &[Value::from_string("world".to_string())], ); diff --git a/src/runtime/interpreter_core_tests_part2.rs b/src/runtime/interpreter_core_tests_part2.rs index c746fdb35..0724f546e 100644 --- a/src/runtime/interpreter_core_tests_part2.rs +++ b/src/runtime/interpreter_core_tests_part2.rs @@ -958,8 +958,9 @@ fn test_format_debug_incomplete() { "{:?x".to_string(), )))], }); - let result = interp.eval_expr(&expr).unwrap(); - assert!(matches!(result, Value::String(_))); + // FMTSPEC-1: an unterminated field is an error, as in rustc. + let err = interp.eval_expr(&expr).unwrap_err(); + assert!(err.to_string().contains("unterminated"), "{err}"); } #[test] @@ -972,8 +973,9 @@ fn test_format_colon_only() { "{:x".to_string(), )))], }); - let result = interp.eval_expr(&expr).unwrap(); - assert!(matches!(result, Value::String(_))); + // FMTSPEC-1: an unterminated field is an error, as in rustc. + let err = interp.eval_expr(&expr).unwrap_err(); + assert!(err.to_string().contains("unterminated"), "{err}"); } #[test] @@ -986,12 +988,9 @@ fn test_format_excess_placeholders() { "{} {} {}".to_string(), )))], }); - let result = interp.eval_expr(&expr).unwrap(); - if let Value::String(s) = result { - assert!(s.as_ref().contains("{}")); - } else { - panic!("Expected String"); - } + // FMTSPEC-1: a field with no argument is an error, as in rustc. + let err = interp.eval_expr(&expr).unwrap_err(); + assert!(err.to_string().contains("positional argument"), "{err}"); } // ===== Import Tests ===== diff --git a/src/runtime/interpreter_format_calls.rs b/src/runtime/interpreter_format_calls.rs new file mode 100644 index 000000000..b4cd84789 --- /dev/null +++ b/src/runtime/interpreter_format_calls.rs @@ -0,0 +1,196 @@ +//! FMTSPEC-1 / EPRINTLN-1 / FORMATFN-1: the output and format calls +//! (`println`, `print`, `eprintln`, `eprint`, `format`, function or macro +//! form) all format through [`crate::runtime::fmt_spec`]. +//! +//! Calls made through the interpreter resolve `{name}` against explicit named +//! arguments (`name = expr`) and then variables in scope, as Rust 2021 inline +//! format arguments do. A string literal first argument is always a format +//! string, as in Rust. + +use crate::frontend::ast::{Expr, ExprKind, Literal}; +use crate::runtime::fmt_spec; +use crate::runtime::interpreter::Interpreter; +use crate::runtime::{InterpreterError, Value}; +use std::io::Write; + +/// Where the formatted text goes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatSink { + Print, + Println, + Eprint, + Eprintln, + Return, +} + +impl FormatSink { + /// The sink of a builtin output/format function or macro name. + pub fn for_name(name: &str) -> Option { + match name { + "print" => Some(Self::Print), + "println" => Some(Self::Println), + "eprint" => Some(Self::Eprint), + "eprintln" => Some(Self::Eprintln), + "format" => Some(Self::Return), + _ => None, + } + } + + /// The sink of a `__builtin_*__` marker. + pub fn for_marker(marker: &str) -> Option { + let name = marker.strip_prefix("__builtin_")?.strip_suffix("__")?; + Self::for_name(name) + } + + fn marker(self) -> &'static str { + match self { + Self::Print => "__builtin_print__", + Self::Println => "__builtin_println__", + Self::Eprint => "__builtin_eprint__", + Self::Eprintln => "__builtin_eprintln__", + Self::Return => "__builtin_format__", + } + } +} + +/// Deliver `text` to `sink`; `format` returns it as a string value. +pub fn emit(sink: FormatSink, text: String) -> Value { + match sink { + FormatSink::Return => return Value::from_string(text), + FormatSink::Print => write_stdout(&text), + FormatSink::Println => write_stdout(&format!("{text}\n")), + FormatSink::Eprint => eprint!("{text}"), + FormatSink::Eprintln => eprintln!("{text}"), + } + Value::Nil +} + +fn write_stdout(text: &str) { + // Notebook capture reads the output buffer. + if let Ok(mut buf) = crate::runtime::builtins::OUTPUT_BUFFER.lock() { + buf.push_str(text); + } + print!("{text}"); + let _ = std::io::stdout().flush(); +} + +fn format_error(e: String) -> InterpreterError { + InterpreterError::RuntimeError(format!("format error: {e}")) +} + +/// The builtin path (no scope): `println` called through a value. The first +/// value is a format string when more values follow it. +pub fn eval_builtin_format(sink: FormatSink, args: &[Value]) -> Result { + let template = args.len() > 1; + let text = fmt_spec::format_call(args, template, &|_| None).map_err(format_error)?; + Ok(emit(sink, text)) +} + +fn is_string_literal(expr: &Expr) -> bool { + matches!(expr.kind, ExprKind::Literal(Literal::String(_))) +} + +/// `name = expr` in argument position (not the first argument). +fn named_arg(expr: &Expr) -> Option<(&str, &Expr)> { + match &expr.kind { + ExprKind::Assign { target, value } => match &target.kind { + ExprKind::Identifier(name) => Some((name.as_str(), value.as_ref())), + _ => None, + }, + _ => None, + } +} + +type NamedArgs = Vec<(String, Value)>; + +impl Interpreter { + /// A call expression. Output/format builtins that the program has not + /// shadowed format with scope access; every other call is ordinary. + pub(crate) fn eval_call_expr( + &mut self, + func: &Expr, + args: &[Expr], + ) -> Result { + match self.format_sink_for(func) { + Some(sink) => self.eval_format_call(sink, args), + None => self.eval_function_call(func, args), + } + } + + fn format_sink_for(&self, func: &Expr) -> Option { + let ExprKind::Identifier(name) = &func.kind else { + return None; + }; + let sink = FormatSink::for_name(name)?; + match self.lookup_variable(name) { + Ok(Value::String(marker)) if marker.as_ref() == sink.marker() => Some(sink), + _ => None, + } + } + + /// Format `args` (a format string and its arguments) and deliver the text. + pub(crate) fn eval_format_call( + &mut self, + sink: FormatSink, + args: &[Expr], + ) -> Result { + let (positional, named) = self.eval_format_args(args)?; + let template = args.first().is_some_and(is_string_literal) || positional.len() > 1; + let resolve = |name: &str| { + named + .iter() + .find(|(n, _)| n == name) + .map(|(_, v)| v.clone()) + .or_else(|| self.lookup_variable(name).ok()) + }; + let text = fmt_spec::format_call(&positional, template, &resolve).map_err(format_error)?; + Ok(emit(sink, text)) + } + + fn eval_format_args( + &mut self, + args: &[Expr], + ) -> Result<(Vec, NamedArgs), InterpreterError> { + let mut positional = Vec::new(); + let mut named = Vec::new(); + for (i, arg) in args.iter().enumerate() { + match named_arg(arg).filter(|_| i > 0) { + Some((name, value)) => named.push((name.to_string(), self.eval_expr(value)?)), + None => positional.push(self.eval_expr(arg)?), + } + } + Ok((positional, named)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fmtspec_1_sink_names_and_markers() { + for name in ["print", "println", "eprint", "eprintln", "format"] { + let sink = FormatSink::for_name(name).expect(name); + assert_eq!(FormatSink::for_marker(sink.marker()), Some(sink)); + } + assert_eq!(FormatSink::for_name("dbg"), None); + assert_eq!(FormatSink::for_marker("println"), None); + } + + #[test] + fn test_formatfn_1_builtin_format_returns_string() { + let args = [ + Value::from_string("{:?}".to_string()), + Value::from_array(vec![Value::Integer(1), Value::Integer(2)]), + ]; + let got = eval_builtin_format(FormatSink::Return, &args).unwrap(); + assert_eq!(got, Value::from_string("[1, 2]".to_string())); + } + + #[test] + fn test_fmtspec_1_builtin_format_error_is_runtime_error() { + let args = [Value::from_string("{:1$}".to_string()), Value::Integer(1)]; + let err = eval_builtin_format(FormatSink::Return, &args).unwrap_err(); + assert!(format!("{err:?}").contains("1$")); + } +} diff --git a/src/runtime/interpreter_misc_eval.rs b/src/runtime/interpreter_misc_eval.rs index b1e460d25..75e29ac0e 100644 --- a/src/runtime/interpreter_misc_eval.rs +++ b/src/runtime/interpreter_misc_eval.rs @@ -212,24 +212,28 @@ fn eval_import(interp: &mut Interpreter, module: &str) -> Result Result { if name == "vec" { - eval_vec_macro(interp, args) - } else if name == "println" { - eval_println_macro(interp, args) - } else if name == "format" { - eval_format_macro(interp, args) - } else { - Err(InterpreterError::RuntimeError(format!( + return eval_vec_macro(interp, args); + } + match crate::runtime::interpreter_format_calls::FormatSink::for_name(name) { + Some(crate::runtime::interpreter_format_calls::FormatSink::Return) if args.is_empty() => { + Err(InterpreterError::RuntimeError( + "format!() requires at least one argument".to_string(), + )) + } + Some(sink) => interp.eval_format_call(sink, args), + None => Err(InterpreterError::RuntimeError(format!( "Macro '{}!' not yet implemented", name - ))) + ))), } } @@ -243,58 +247,6 @@ fn eval_vec_macro(interp: &mut Interpreter, args: &[Expr]) -> Result Result { - if args.is_empty() { - println!(); - } else if args.len() == 1 { - // Single argument: print directly - let value = interp.eval_expr(&args[0])?; - println!("{}", value); - } else { - // Multiple arguments: use format! logic (Issue #82, #83) - let format_val = interp.eval_expr(&args[0])?; - let format_str = match format_val { - Value::String(ref s) => s.as_ref().to_string(), - _ => format_val.to_string(), - }; - - let mut values = Vec::new(); - for arg in &args[1..] { - values.push(interp.eval_expr(arg)?); - } - - // Use helper for format string replacement - let result = Interpreter::format_string_with_values(&format_str, &values); - println!("{}", result); - } - Ok(Value::Nil) -} - -/// format!() macro: Format string with placeholders (Issue #83) -fn eval_format_macro(interp: &mut Interpreter, args: &[Expr]) -> Result { - if args.is_empty() { - return Err(InterpreterError::RuntimeError( - "format!() requires at least one argument".to_string(), - )); - } - - // Evaluate format string (the raw text, not its quoted Display) - let format_val = interp.eval_expr(&args[0])?; - let format_str = crate::runtime::value_format::format_value_display(&format_val); - - // Evaluate remaining arguments - let mut values = Vec::new(); - for arg in &args[1..] { - values.push(interp.eval_expr(arg)?); - } - - // RHLGA-1: same substitution as println!() ({} raw, {:?} Rust Debug) - let result = Interpreter::format_string_with_values(&format_str, &values); - Ok(Value::from_string(result)) -} - // ============================================================================ // Try operator (?) // ============================================================================ diff --git a/src/runtime/interpreter_tests/interpreter_tests_part10.rs b/src/runtime/interpreter_tests/interpreter_tests_part10.rs index 35172253d..3f71cfa87 100644 --- a/src/runtime/interpreter_tests/interpreter_tests_part10.rs +++ b/src/runtime/interpreter_tests/interpreter_tests_part10.rs @@ -849,7 +849,8 @@ fn test_format_macro_excess_placeholders() { let mut interp = Interpreter::new(); // More placeholders than values let result = interp.eval_string(r#"format!("{} {} {}", 1)"#); - assert!(result.is_ok()); // Should preserve extra placeholders + // FMTSPEC-1: rustc rejects missing arguments; so does the interpreter. + assert!(result.is_err()); } #[test] diff --git a/src/runtime/interpreter_tests/interpreter_tests_part6.rs b/src/runtime/interpreter_tests/interpreter_tests_part6.rs index 124ecab00..904c8daa0 100644 --- a/src/runtime/interpreter_tests/interpreter_tests_part6.rs +++ b/src/runtime/interpreter_tests/interpreter_tests_part6.rs @@ -1006,12 +1006,10 @@ fn test_format_macro_missing_values() { trailing_comment: None, contracts: Vec::new(), }; - let result = interp.eval_expr(¯o_expr).expect("should evaluate"); - if let Value::String(s) = result { - assert!(s.contains("{}")); - } else { - panic!("Expected string"); - } + // FMTSPEC-1: a field with no argument is an error (rustc rejects it), + // not the placeholder text. + let err = interp.eval_expr(¯o_expr).unwrap_err(); + assert!(err.to_string().contains("positional argument"), "{err}"); } // ============== Lookup Variable Special Cases ============== diff --git a/src/runtime/interpreter_tests/interpreter_tests_part9.rs b/src/runtime/interpreter_tests/interpreter_tests_part9.rs index b40939b66..a4a252941 100644 --- a/src/runtime/interpreter_tests/interpreter_tests_part9.rs +++ b/src/runtime/interpreter_tests/interpreter_tests_part9.rs @@ -432,7 +432,7 @@ fn test_binary_op_gt() { #[test] fn test_format_string_with_values() { - let result = Interpreter::format_string_with_values( + let result = crate::runtime::value_format::format_string_with_values( "x={}, y={}", &[Value::Integer(10), Value::Integer(20)], ); diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 595732623..530ab1057 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -114,8 +114,10 @@ pub mod eval_string; pub mod eval_string_interpolation; pub mod eval_string_methods; pub mod eval_try_catch; // EXTREME TDD: Try/catch error handling +pub mod fmt_spec; // FMTSPEC-1: Rust std::fmt format strings pub mod gc; pub mod gc_impl; // EXTREME TDD: Full GC implementation with tests +pub mod interpreter_format_calls; // FMTSPEC-1: println/print/eprintln/eprint/format pub mod type_feedback; // EXTREME TDD: JIT type feedback system (extracted from interpreter.rs) pub mod validation; pub mod value_format; // EXTREME TDD: Value formatting utilities (extracted from interpreter.rs) diff --git a/src/runtime/value_format.rs b/src/runtime/value_format.rs index 90715b071..dda85b358 100644 --- a/src/runtime/value_format.rs +++ b/src/runtime/value_format.rs @@ -55,23 +55,14 @@ pub fn format_string_with_values(format_str: &str, values: &[Value]) -> String { result } -/// Format a value with a format specifier like `:.2` for floats +/// Format a value with a format specifier like `:.2` (f-string `{x:.2}`). +/// FMTSPEC-1: the spec is parsed by [`crate::runtime::fmt_spec`], the one +/// format-spec parser; a spec it refuses leaves the value's plain text. pub fn format_value_with_spec(value: &Value, spec: &str) -> String { - // Parse format specifier (e.g., ":.2" -> precision 2) - if let Some(stripped) = spec.strip_prefix(":.") { - if let Ok(precision) = stripped.parse::() { - match value { - Value::Float(f) => return format!("{f:.precision$}"), - Value::Integer(i) => { - let f = *i as f64; - return format!("{f:.precision$}"); - } - _ => {} - } - } - } - // Default formatting if spec doesn't match or isn't supported - value.to_string() + let text = spec.strip_prefix(':').unwrap_or(spec); + crate::runtime::fmt_spec::parse_spec(text) + .and_then(|spec| crate::runtime::fmt_spec::format_value(value, &spec)) + .unwrap_or_else(|_| value.to_string()) } /// Format value for debug output (`{:?}`), as rustc prints the transpiled value. @@ -301,8 +292,9 @@ mod tests { #[test] fn test_spec_integer_as_float() { + // FMTSPEC-1: Rust ignores precision for integers let result = format_value_with_spec(&Value::Integer(42), ":.2"); - assert_eq!(result, "42.00"); + assert_eq!(result, format!("{:.2}", 42)); } #[test] @@ -326,13 +318,14 @@ mod tests { #[test] fn test_spec_non_numeric_value() { let result = format_value_with_spec(&Value::from_string("hello".to_string()), ":.2"); - assert_eq!(result, "\"hello\""); + // FMTSPEC-1: precision truncates a string, as Rust does + assert_eq!(result, format!("{:.2}", "hello")); } #[test] fn test_spec_bool_value() { let result = format_value_with_spec(&Value::Bool(true), ":.2"); - assert_eq!(result, "true"); + assert_eq!(result, format!("{:.2}", true)); } #[test] @@ -501,7 +494,7 @@ mod tests { #[test] fn test_spec_integer_precision_3() { let result = format_value_with_spec(&Value::Integer(100), ":.3"); - assert_eq!(result, "100.000"); + assert_eq!(result, format!("{:.3}", 100)); } #[test] From e863979fbba83da28bce4e453dbd1fe100098346 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:24:29 +0200 Subject: [PATCH 078/106] RHLGA-1: EPRINTLN-1 eprintln/eprint builtins write formatted text to stderr - builtin_init registers eprintln and eprint (were "Undefined function") - they share the FMTSPEC-1 engine with println/print Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 534 +++++++++++++++++++++++------------- src/runtime/builtin_init.rs | 16 +- 2 files changed, 360 insertions(+), 190 deletions(-) diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 586c6d716..299b31ef5 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T09:27:48.275323465Z", + "created_at": "2026-09-24T09:27:49.944844442Z", "git_context": null, "files": { "./build.rs": { @@ -33814,48 +33814,48 @@ }, "./src/runtime/eval_builtin.rs": { "content_hash": [ - 66, - 24, + 138, + 227, + 181, + 181, + 50, + 100, + 44, + 142, + 149, + 245, + 17, + 117, + 11, + 151, + 7, + 146, + 112, + 150, + 113, + 117, 26, - 160, - 118, - 179, - 80, - 97, + 2, + 38, 26, - 134, - 194, - 115, - 201, - 55, - 17, - 81, - 164, - 32, - 77, - 206, - 237, - 210, - 216, - 147, - 54, - 111, - 245, - 127, - 182, - 232, - 231, - 66 + 98, + 171, + 157, + 114, + 15, + 219, + 78, + 177 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 16.67304, + "duplication_ratio": 16.709108, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 76.67304, + "total": 76.70911, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -33867,15 +33867,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 60 duplicate code patterns" + "issue": "Found 55 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.3269598, + "amount": 3.2908914, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 16.6%" + "issue": "Code duplication: 16.5%" }, { "source_metric": "StructuralComplexity", @@ -33883,7 +33883,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 134" + "issue": "High cognitive complexity: 127" }, { "source_metric": "StructuralComplexity", @@ -33891,7 +33891,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 239" + "issue": "High cyclomatic complexity: 234" } ], "critical_defects_count": 0, @@ -36536,6 +36536,93 @@ }, "git_context": null }, + "./src/runtime/fmt_spec.rs": { + "content_hash": [ + 202, + 42, + 183, + 191, + 148, + 74, + 29, + 14, + 218, + 233, + 179, + 161, + 51, + 53, + 35, + 188, + 175, + 199, + 51, + 214, + 75, + 19, + 41, + 253, + 203, + 95, + 225, + 113, + 80, + 33, + 46, + 211 + ], + "score": { + "structural_complexity": 0.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 5.0, + "total": 80.0, + "grade": "B+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/runtime/fmt_spec.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 5.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 34 duplicate code patterns" + }, + { + "source_metric": "StructuralComplexity", + "amount": 10.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cognitive complexity: 54" + }, + { + "source_metric": "StructuralComplexity", + "amount": 15.0, + "applied_to": [ + "StructuralComplexity" + ], + "issue": "High cyclomatic complexity: 108" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/runtime/gc.rs": { "content_hash": [ 200, @@ -36894,48 +36981,48 @@ }, "./src/runtime/interpreter.rs": { "content_hash": [ - 235, - 176, - 143, - 176, - 209, - 29, - 242, - 232, - 81, - 144, - 134, - 137, - 222, - 202, - 4, - 59, - 124, - 75, - 185, - 170, - 75, - 219, - 122, - 43, - 253, + 23, + 61, + 6, + 5, + 114, + 67, + 3, + 252, + 56, + 45, + 53, + 189, + 146, + 72, + 250, + 37, + 30, 203, 67, - 138, - 63, - 201, - 61, - 151 + 102, + 152, + 98, + 186, + 70, + 33, + 89, + 244, + 43, + 176, + 96, + 165, + 222 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 20.0, - "duplication_ratio": 17.538237, + "duplication_ratio": 17.532846, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 77.53824, + "total": 77.532845, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -36951,7 +37038,7 @@ }, { "source_metric": "Duplication", - "amount": 2.4617627, + "amount": 2.4671533, "applied_to": [ "Duplication" ], @@ -37185,6 +37272,77 @@ }, "git_context": null }, + "./src/runtime/interpreter_format_calls.rs": { + "content_hash": [ + 38, + 187, + 141, + 98, + 19, + 34, + 99, + 0, + 152, + 100, + 187, + 66, + 53, + 243, + 167, + 180, + 174, + 39, + 96, + 212, + 74, + 251, + 234, + 254, + 223, + 175, + 243, + 168, + 113, + 184, + 157, + 111 + ], + "score": { + "structural_complexity": 25.0, + "semantic_complexity": 20.0, + "duplication_ratio": 20.0, + "coupling_score": 15.0, + "doc_coverage": 10.0, + "consistency_score": 10.0, + "entropy_score": 6.0, + "total": 96.36363, + "grade": "A+", + "confidence": 1.0, + "language": "Rust", + "file_path": "./src/runtime/interpreter_format_calls.rs", + "penalties_applied": [ + { + "source_metric": "Duplication", + "amount": 4.0, + "applied_to": [ + "Duplication" + ], + "issue": "Found 8 duplicate code patterns" + } + ], + "critical_defects_count": 0, + "has_critical_defects": false, + "has_contract_coverage": false + }, + "components": { + "complexity_breakdown": {}, + "duplication_sources": [], + "coupling_dependencies": [], + "doc_missing_items": [], + "consistency_violations": [] + }, + "git_context": null + }, "./src/runtime/interpreter_functions.rs": { "content_hash": [ 82, @@ -37930,48 +38088,48 @@ }, "./src/runtime/interpreter_misc_eval.rs": { "content_hash": [ - 161, - 62, - 56, - 225, - 38, - 172, - 125, - 140, - 251, - 38, - 242, - 238, 43, - 114, + 151, + 82, + 214, + 107, + 235, + 157, + 75, + 250, + 178, + 68, + 196, + 51, + 160, + 186, + 4, 6, - 54, - 133, - 141, - 25, - 248, - 97, - 173, - 44, - 56, - 233, - 132, - 171, - 45, - 108, - 202, - 113, - 142 + 220, + 226, + 213, + 128, + 145, + 107, + 188, + 223, + 89, + 228, + 152, + 239, + 31, + 196, + 178 ], "score": { "structural_complexity": 0.0, "semantic_complexity": 18.5, - "duplication_ratio": 16.57444, + "duplication_ratio": 16.639118, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 75.07444, + "total": 75.139114, "grade": "B", "confidence": 1.0, "language": "Rust", @@ -37983,15 +38141,15 @@ "applied_to": [ "Duplication" ], - "issue": "Found 102 duplicate code patterns" + "issue": "Found 95 duplicate code patterns" }, { "source_metric": "Duplication", - "amount": 3.42556, + "amount": 3.3608816, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 17.1%" + "issue": "Code duplication: 16.8%" }, { "source_metric": "StructuralComplexity", @@ -37999,7 +38157,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 99" + "issue": "High cognitive complexity: 84" }, { "source_metric": "StructuralComplexity", @@ -38007,7 +38165,7 @@ "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 117" + "issue": "High cyclomatic complexity: 111" }, { "source_metric": "SemanticComplexity", @@ -38926,48 +39084,48 @@ }, "./src/runtime/mod.rs": { "content_hash": [ - 138, - 209, - 160, - 254, - 233, - 168, - 64, - 16, - 10, - 55, - 69, - 255, - 4, - 121, - 63, - 123, - 192, - 53, - 176, - 13, - 160, - 238, - 142, + 221, + 32, + 51, + 120, + 157, + 14, + 211, + 127, + 222, + 170, + 1, + 239, + 165, + 134, + 240, + 236, + 164, + 59, + 175, + 92, 45, - 243, - 213, - 15, - 99, - 94, - 62, - 156, - 90 + 233, + 185, + 197, + 117, + 44, + 18, + 149, + 206, + 33, + 173, + 0 ], "score": { "structural_complexity": 25.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.377575, + "duplication_ratio": 15.398633, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 91.25234, + "total": 91.27149, "grade": "A", "confidence": 1.0, "language": "Rust", @@ -38983,11 +39141,11 @@ }, { "source_metric": "Duplication", - "amount": 4.6224256, + "amount": 4.601367, "applied_to": [ "Duplication" ], - "issue": "Code duplication: 23.1%" + "issue": "Code duplication: 23.0%" } ], "critical_defects_count": 0, @@ -40869,49 +41027,49 @@ }, "./src/runtime/value_format.rs": { "content_hash": [ - 121, - 135, - 132, - 254, - 90, - 24, - 94, - 254, - 83, - 102, - 160, - 57, - 28, - 177, - 173, - 13, - 85, - 255, - 66, + 158, + 93, + 72, + 142, + 101, 110, - 180, - 91, - 246, - 19, 113, - 199, - 19, - 199, - 205, - 191, - 186, - 92 + 245, + 3, + 109, + 231, + 40, + 226, + 34, + 147, + 135, + 235, + 102, + 47, + 58, + 162, + 118, + 27, + 114, + 168, + 230, + 23, + 17, + 216, + 156, + 35, + 239 ], "score": { - "structural_complexity": 8.0, + "structural_complexity": 11.799999, "semantic_complexity": 20.0, "duplication_ratio": 20.0, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 88.0, - "grade": "A-", + "total": 91.8, + "grade": "A", "confidence": 1.0, "language": "Rust", "file_path": "./src/runtime/value_format.rs", @@ -40930,23 +41088,23 @@ "applied_to": [ "Duplication" ], - "issue": "Found 33 duplicate code patterns" + "issue": "Found 32 duplicate code patterns" }, { "source_metric": "StructuralComplexity", - "amount": 10.0, + "amount": 8.700001, "applied_to": [ "StructuralComplexity" ], - "issue": "High cognitive complexity: 50" + "issue": "High cognitive complexity: 44" }, { "source_metric": "StructuralComplexity", - "amount": 5.0, + "amount": 2.5, "applied_to": [ "StructuralComplexity" ], - "issue": "High cyclomatic complexity: 40" + "issue": "High cyclomatic complexity: 35" } ], "critical_defects_count": 0, @@ -45061,13 +45219,13 @@ } }, "summary": { - "total_files": 530, - "avg_score": 90.27265, + "total_files": 532, + "avg_score": 90.272156, "grade_distribution": { - "A+": 195, - "A": 174, - "A-": 34, - "B+": 29, + "A+": 196, + "A": 175, + "A-": 33, + "B+": 30, "B": 69, "B-": 26, "C": 1, @@ -45076,7 +45234,7 @@ "languages": { "JavaScript": 14, "Python": 4, - "Rust": 508, + "Rust": 510, "TypeScript": 4 } } diff --git a/src/runtime/builtin_init.rs b/src/runtime/builtin_init.rs index e12d4e878..097d0f536 100644 --- a/src/runtime/builtin_init.rs +++ b/src/runtime/builtin_init.rs @@ -123,6 +123,15 @@ fn add_io_functions(global_env: &mut HashMap) { "print".to_string(), Value::from_string("__builtin_print__".to_string()), ); + // EPRINTLN-1: stderr counterparts of println/print + global_env.insert( + "eprintln".to_string(), + Value::from_string("__builtin_eprintln__".to_string()), + ); + global_env.insert( + "eprint".to_string(), + Value::from_string("__builtin_eprint__".to_string()), + ); global_env.insert( "dbg".to_string(), Value::from_string("__builtin_dbg__".to_string()), @@ -814,7 +823,8 @@ mod tests { // parse_json alias: parse_json (Issue #131 - v3.182.0) // +1 dir() builtin (object-inspection-consistency spec) // +1 assert_ne() builtin (METHODS-1) - assert_eq!(env.len(), 118); + // +2 eprintln()/eprint() builtins (EPRINTLN-1) + assert_eq!(env.len(), 120); } #[test] @@ -898,7 +908,9 @@ mod tests { assert!(env.contains_key("println")); assert!(env.contains_key("print")); assert!(env.contains_key("dbg")); - assert_eq!(env.len(), 3); + assert!(env.contains_key("eprintln")); + assert!(env.contains_key("eprint")); + assert_eq!(env.len(), 5); } #[test] From 1d1e7594700314cb14d3c962ff7e81d2e4759cc4 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:24:33 +0200 Subject: [PATCH 079/106] RHLGA-1: DICTIDX-1 string keys at any link of an index chain - eval_index_assign reads the container, rebuilds it with the element (an integer index into an array, or a string key into an object/dict) and writes it back through write_back: d["k"] = v, d["k"] += 1, d["k"].push(2), m["a"]["b"].push(3), v[0]["x"] += 10 - replaces assign_nested_array and the field-chain special case - tests/g2b_runfix_2.rs: FMTSPEC-1/EPRINTLN-1/FORMATFN-1/DICTIDX-1 cases, each proved against a rustc-compiled Rust reference Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .pmat/baseline.json | 68 ++--- src/runtime/interpreter_control_flow.rs | 209 ++++--------- tests/g2b_runfix_2.rs | 380 ++++++++++++++++++++++++ 3 files changed, 466 insertions(+), 191 deletions(-) create mode 100644 tests/g2b_runfix_2.rs diff --git a/.pmat/baseline.json b/.pmat/baseline.json index 299b31ef5..71da28304 100644 --- a/.pmat/baseline.json +++ b/.pmat/baseline.json @@ -1,6 +1,6 @@ { "version": "3.41.1", - "created_at": "2026-09-24T09:27:49.944844442Z", + "created_at": "2026-09-24T09:27:51.419412572Z", "git_context": null, "files": { "./build.rs": { @@ -32660,48 +32660,48 @@ }, "./src/runtime/builtin_init.rs": { "content_hash": [ - 71, - 240, - 111, - 6, - 57, - 3, - 241, - 181, - 106, - 249, - 4, 27, - 134, - 245, - 52, - 200, - 166, - 203, - 197, - 208, - 211, - 102, - 176, - 146, + 113, + 250, + 88, + 59, + 70, + 2, 9, - 105, - 228, - 117, 24, - 13, - 119, - 31 + 147, + 180, + 138, + 7, + 230, + 128, + 27, + 81, + 84, + 137, + 236, + 152, + 240, + 48, + 70, + 157, + 227, + 151, + 8, + 70, + 167, + 93, + 234 ], "score": { "structural_complexity": 8.0, "semantic_complexity": 20.0, - "duplication_ratio": 15.33933, + "duplication_ratio": 15.344687, "coupling_score": 15.0, "doc_coverage": 10.0, "consistency_score": 10.0, "entropy_score": 5.0, - "total": 83.339325, + "total": 83.34469, "grade": "B+", "confidence": 1.0, "language": "Rust", @@ -32717,7 +32717,7 @@ }, { "source_metric": "Duplication", - "amount": 4.6606703, + "amount": 4.6553125, "applied_to": [ "Duplication" ], @@ -45220,7 +45220,7 @@ }, "summary": { "total_files": 532, - "avg_score": 90.272156, + "avg_score": 90.27216, "grade_distribution": { "A+": 196, "A": 175, diff --git a/src/runtime/interpreter_control_flow.rs b/src/runtime/interpreter_control_flow.rs index c876209a8..21a45bdc2 100644 --- a/src/runtime/interpreter_control_flow.rs +++ b/src/runtime/interpreter_control_flow.rs @@ -430,60 +430,6 @@ impl Interpreter { } } - /// BUG-003: Evaluate array/vector index assignment (arr[i] = value, matrix[i][j] = value) - /// - /// Handles both simple (arr[0] = 99) and nested (matrix[0][1] = 99) index assignment. - /// Complexity: 8 (≤10 target) - /// Assign a value into a 2D array: `arr[outer_idx][inner_idx] = val`. - fn assign_nested_array( - &mut self, - arr_name: &str, - outer_idx: usize, - inner_idx_val: Value, - val: Value, - ) -> Result { - let inner_idx = match inner_idx_val { - Value::Integer(i) => i as usize, - _ => { - return Err(InterpreterError::RuntimeError( - "Array index must be an integer".to_string(), - )) - } - }; - - let arr = self.lookup_variable(arr_name)?; - let Value::Array(ref outer_vec) = arr else { - return Err(InterpreterError::RuntimeError( - "Cannot index non-array value".to_string(), - )); - }; - - let mut new_outer = outer_vec.to_vec(); - if outer_idx >= new_outer.len() { - return Err(InterpreterError::RuntimeError(format!( - "Outer index {outer_idx} out of bounds" - ))); - } - - let Value::Array(ref inner_vec) = new_outer[outer_idx] else { - return Err(InterpreterError::RuntimeError( - "Cannot index non-array value".to_string(), - )); - }; - - let mut new_inner = inner_vec.to_vec(); - if inner_idx >= new_inner.len() { - return Err(InterpreterError::RuntimeError(format!( - "Inner index {inner_idx} out of bounds" - ))); - } - - new_inner[inner_idx] = val.clone(); - new_outer[outer_idx] = Value::Array(Arc::from(new_inner)); - self.set_variable(arr_name, Value::Array(Arc::from(new_outer))); - Ok(val) - } - /// Evaluate field assignment: `obj.field = value`. /// /// Handles Object, ObjectMut, Class, and Struct field updates. @@ -539,112 +485,34 @@ impl Interpreter { } } - /// Element assignment `object[index] = val`. An array reached through a - /// chain containing a field (`o.items[0] = x`, `g.grid[0][1] = x`) is - /// rebuilt and written back through the chain; every other object uses - /// [`Self::eval_index_assign`]. + /// Element assignment `object[index] = val`: delegates to + /// [`Self::eval_index_assign`], the single element writer. fn eval_element_assign( &mut self, object: &Expr, index: &Expr, val: Value, ) -> Result { - if !is_chain_through_field(object) { - return self.eval_index_assign(object, index, val); - } - let Value::Array(arr) = self.eval_expr(object)? else { - return Err(InterpreterError::RuntimeError( - "Cannot index non-array value".to_string(), - )); - }; - let Value::Integer(i) = self.eval_expr(index)? else { - return Err(InterpreterError::RuntimeError( - "Array index must be an integer".to_string(), - )); - }; - let mut items = arr.to_vec(); - let len = items.len(); - let slot = usize::try_from(i).ok().and_then(|i| items.get_mut(i)); - let Some(slot) = slot else { - return Err(InterpreterError::RuntimeError(format!( - "Index {i} out of bounds for array of length {len}" - ))); - }; - *slot = val.clone(); - self.write_back(object, Value::Array(Arc::from(items)))?; - Ok(val) + self.eval_index_assign(object, index, val) } + /// BUG-003 / NESTASSIGN-1 / DICTIDX-1: `object[index] = val` where + /// `object` is any place (`v`, `m[0]`, `o.items`, `d["k"]["j"]`). The + /// container is read, rebuilt with the new element (an integer index into + /// an array, or a string key into an object/dict) and written back + /// through `write_back`, so every link of the chain is updated. + /// Complexity: 1 pub(crate) fn eval_index_assign( &mut self, object: &Expr, index: &Expr, val: Value, ) -> Result { - match &object.kind { - ExprKind::Identifier(arr_name) => { - // Simple case: arr[i] = value - let idx_val = self.eval_expr(index)?; - let idx = match idx_val { - Value::Integer(i) => i as usize, - _ => { - return Err(InterpreterError::RuntimeError( - "Array index must be an integer".to_string(), - )) - } - }; - - let arr = self.lookup_variable(arr_name)?; - match arr { - Value::Array(ref vec) => { - let mut new_vec = vec.to_vec(); - if idx < new_vec.len() { - new_vec[idx] = val.clone(); - self.set_variable(arr_name, Value::Array(Arc::from(new_vec))); - Ok(val) - } else { - Err(InterpreterError::RuntimeError(format!( - "Index {} out of bounds for array of length {}", - idx, - new_vec.len() - ))) - } - } - _ => Err(InterpreterError::RuntimeError( - "Cannot index non-array value".to_string(), - )), - } - } - ExprKind::IndexAccess { - object: nested_obj, - index: nested_idx, - } => { - // Nested case: matrix[i][j] = value - // Get the outer index first - let outer_idx_val = self.eval_expr(nested_idx)?; - let outer_idx = match outer_idx_val { - Value::Integer(i) => i as usize, - _ => { - return Err(InterpreterError::RuntimeError( - "Array index must be an integer".to_string(), - )) - } - }; - - // Get the root array name (only handle Identifier for now) - if let ExprKind::Identifier(arr_name) = &nested_obj.kind { - let inner_idx_val = self.eval_expr(index)?; - self.assign_nested_array(arr_name, outer_idx, inner_idx_val, val) - } else { - Err(InterpreterError::RuntimeError( - "Complex nested index assignment not yet supported".to_string(), - )) - } - } - _ => Err(InterpreterError::RuntimeError( - "Complex array assignment targets not yet supported".to_string(), - )), - } + let container = self.eval_expr(object)?; + let key = self.eval_expr(index)?; + let updated = with_element(container, &key, val.clone())?; + self.write_back(object, updated)?; + Ok(val) } /// Evaluate a compound assignment (`x += 1`, `o.a.b -= 2`, `v[i] *= 3`, @@ -674,17 +542,38 @@ impl Interpreter { } } -/// IDXCOMPOUND-1: `expr` is a FieldAccess, or an IndexAccess chain with a -/// FieldAccess somewhere below it (`o.grid[0]`). -/// Complexity: 3 -fn is_chain_through_field(expr: &Expr) -> bool { - match &expr.kind { - ExprKind::FieldAccess { .. } => true, - ExprKind::IndexAccess { object, .. } => is_chain_through_field(object), - _ => false, +/// DICTIDX-1: `container` with the element at `key` replaced by `val`: an +/// integer index into an array, or a string key into an object/dict. +/// Complexity: 4 +fn with_element(container: Value, key: &Value, val: Value) -> Result { + match (&container, key) { + (Value::Array(items), Value::Integer(i)) => with_array_slot(items, *i, val), + (Value::Array(_), _) => Err(InterpreterError::RuntimeError( + "Array index must be an integer".to_string(), + )), + (_, Value::String(k)) => { + super::interpreter_methods_dispatch::with_field(container.clone(), k, val) + } + _ => Err(InterpreterError::RuntimeError( + "Cannot index non-array value".to_string(), + )), } } +/// `items` with slot `i` replaced by `val`; an out-of-range index is an error. +/// Complexity: 2 +fn with_array_slot(items: &[Value], i: i64, val: Value) -> Result { + let mut items = items.to_vec(); + let len = items.len(); + let Some(slot) = usize::try_from(i).ok().and_then(|i| items.get_mut(i)) else { + return Err(InterpreterError::RuntimeError(format!( + "Index {i} out of bounds for array of length {len}" + ))); + }; + *slot = val; + Ok(Value::Array(Arc::from(items))) +} + /// IDXCOMPOUND-1 / IDXPUSHWB-1: `expr` names a place: a variable, or a /// FieldAccess/IndexAccess chain whose root is a variable. /// Complexity: 3 @@ -1600,7 +1489,10 @@ mod tests { let result = interp.eval_index_assign(&outer_access, &outer_idx, Value::Integer(99)); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Outer index")); + assert!(result + .unwrap_err() + .to_string() + .contains("Index 5 out of bounds")); } #[test] @@ -1622,7 +1514,10 @@ mod tests { let result = interp.eval_index_assign(&outer_access, &outer_idx, Value::Integer(99)); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Inner index")); + assert!(result + .unwrap_err() + .to_string() + .contains("Index 5 out of bounds")); } #[test] @@ -1660,7 +1555,7 @@ mod tests { assert!(result .unwrap_err() .to_string() - .contains("not yet supported")); + .contains("Cannot index non-array")); } // ============================================================================ diff --git a/tests/g2b_runfix_2.rs b/tests/g2b_runfix_2.rs new file mode 100644 index 000000000..70e507235 --- /dev/null +++ b/tests/g2b_runfix_2.rs @@ -0,0 +1,380 @@ +//! RHLGA-1 G2b run fixes, part 2: four interpreter defects measured with +//! `ruchy run` at a5dc8398. +//! +//! - FMTSPEC-1: the format engine substituted only `{}`/`{:?}`; +//! `println("[{:>5}] [{:.2}] [{0}]", 7, 3.14159)` printed the spec text +//! and appended the values. Rust prints `[ 7] [3.14] [7]`. +//! - EPRINTLN-1: `eprintln`/`eprint` were "Undefined function". +//! - FORMATFN-1 (interpreter half): `format("{:?}", [1, 2])` failed with +//! "Unknown builtin __builtin_format__". +//! - DICTIDX-1 (interpreter half): `d["k"].push(2)` on an object failed with +//! "Array index must be an integer"; string keys now work at any link of an +//! index/field chain for read, assignment, compound assignment and in-place +//! methods. +//! +//! Each case states the output Rust prints for the same program. A +//! hand-written Rust reference is compiled with rustc to prove that string, +//! and `ruchy run` must print it too. `ruchy compile` parity is the +//! transpiler half, fixed in another worktree. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +struct Output { + stdout: String, + stderr: String, +} + +fn output_of(mut cmd: Command, what: &str) -> Output { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + Output { + stdout: String::from_utf8(out.stdout).expect("utf8 stdout"), + stderr: String::from_utf8(out.stderr).expect("utf8 stderr"), + } +} + +fn ruchy_cmd(dir: &Path, src: &str) -> Command { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.arg("run").arg(file).timeout(Duration::from_secs(10)); + cmd +} + +fn rustc_run(dir: &Path, rust: &str) -> Output { + let main_rs = dir.join("reference.rs"); + let bin = dir.join("reference_bin"); + std::fs::write(&main_rs, rust).expect("write reference.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + output_of(rustc, "rustc on the Rust reference"); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(10)); + output_of(run, "Rust reference binary") +} + +/// Rust prints `expected` for `rust`, and `ruchy run` prints it for `ruchy`. +fn assert_matches_rust(ruchy: &str, rust: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!( + rustc_run(dir.path(), rust).stdout, + expected, + "Rust reference" + ); + let got = output_of(ruchy_cmd(dir.path(), ruchy), "ruchy run"); + assert_eq!(got.stdout, expected, "ruchy run"); +} + +// ---------------------------------------------------------------- FMTSPEC-1 + +#[test] +fn test_fmtspec_1_brief_case_width_precision_positional() { + assert_matches_rust( + r#" +fun main() { + println("[{:>5}] [{:.2}] [{0}]", 7, 3.14159) +} +"#, + r#" +fn main() { + println!("[{:>5}] [{:.2}] [{0}]", 7, 3.14159); +} +"#, + "[ 7] [3.14] [7]\n", + ); +} + +#[test] +fn test_fmtspec_1_inline_named_args_fill_align_sign_zero() { + assert_matches_rust( + r#" +fun main() { + let name = "ab" + let n = 42 + println("[{name:>6}] [{n:<4}] [{n:^6}] [{n:+}] [{n:05}] [{:+06}]", -7) +} +"#, + r#" +fn main() { + let name = "ab"; + let n = 42; + println!("[{name:>6}] [{n:<4}] [{n:^6}] [{n:+}] [{n:05}] [{:+06}]", -7); +} +"#, + "[ ab] [42 ] [ 42 ] [+42] [00042] [-00007]\n", + ); +} + +#[test] +fn test_fmtspec_1_explicit_named_args() { + assert_matches_rust( + r#" +fun main() { + println("{a}-{b}-{a}", a = 1, b = "z") +} +"#, + r#" +fn main() { + println!("{a}-{b}-{a}", a = 1, b = "z"); +} +"#, + "1-z-1\n", + ); +} + +#[test] +fn test_fmtspec_1_radix_and_alternate() { + assert_matches_rust( + r#" +fun main() { + println("{:x} {:X} {:b} {:o} {:#x} {:#b} {:#o} {:08b} {:#06x}", 255, 255, 5, 8, 255, 5, 8, 5, 255) +} +"#, + r#" +fn main() { + println!("{:x} {:X} {:b} {:o} {:#x} {:#b} {:#o} {:08b} {:#06x}", 255, 255, 5, 8, 255, 5, 8, 5, 255); +} +"#, + "ff FF 101 10 0xff 0b101 0o10 00000101 0x00ff\n", + ); +} + +#[test] +fn test_fmtspec_1_floats_exp_width_precision() { + assert_matches_rust( + r#" +fun main() { + println("{:e} [{:10.3}] [{:<10.1}] {:+.2} {:08.2} {:E}", 1234.5, 3.14159, 2.75, 1.5, -3.14159, 0.00012) +} +"#, + r#" +fn main() { + println!("{:e} [{:10.3}] [{:<10.1}] {:+.2} {:08.2} {:E}", 1234.5, 3.14159, 2.75, 1.5, -3.14159, 0.00012); +} +"#, + "1.2345e3 [ 3.142] [2.8 ] +1.50 -0003.14 1.2E-4\n", + ); +} + +#[test] +fn test_fmtspec_1_strings_fill_and_truncate() { + assert_matches_rust( + r#" +fun main() { + println("[{:>8}] [{:*^9}] [{:.3}] [{:-<6}] [{:>6.2}]", "hi", "mid", "abcdef", "x", "hello") +} +"#, + r#" +fn main() { + println!("[{:>8}] [{:*^9}] [{:.3}] [{:-<6}] [{:>6.2}]", "hi", "mid", "abcdef", "x", "hello"); +} +"#, + "[ hi] [***mid***] [abc] [x-----] [ he]\n", + ); +} + +#[test] +fn test_fmtspec_1_escapes_debug_and_pretty_debug() { + assert_matches_rust( + r#" +fun main() { + println("{{}} {{{}}} {:?} {:>5?}|", 1, "q", 3) + println("{:#?}", [1, 2]) +} +"#, + r#" +fn main() { + println!("{{}} {{{}}} {:?} {:>5?}|", 1, "q", 3); + println!("{:#?}", vec![1, 2]); +} +"#, + "{} {1} \"q\" 3|\n[\n 1,\n 2,\n]\n", + ); +} + +#[test] +fn test_fmtspec_1_format_macro_and_print_share_the_engine() { + assert_matches_rust( + r#" +fun main() { + let w = 3 + let s = format!("<{:>4}|{w:02}>", "a") + print("{:<3}|", 1) + println("{}", s) +} +"#, + r#" +fn main() { + let w = 3; + let s = format!("<{:>4}|{w:02}>", "a"); + print!("{:<3}|", 1); + println!("{}", s); +} +"#, + "1 |< a|03>\n", + ); +} + +#[test] +fn test_fmtspec_1_width_from_argument_is_an_error_not_wrong_output() { + let dir = tempfile::tempdir().expect("tempdir"); + let out = ruchy_cmd( + dir.path(), + "fun main() {\n println(\"[{:1$}]\", 7, 5)\n}\n", + ) + .output() + .expect("spawn ruchy"); + assert!(!out.status.success(), "must fail, got {:?}", out); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("1$"), "error must name the spec: {stderr}"); + assert!(!String::from_utf8_lossy(&out.stdout).contains('[')); +} + +// ---------------------------------------------------------------- EPRINTLN-1 + +#[test] +fn test_eprintln_1_eprintln_and_eprint_write_stderr() { + let ruchy = r#" +fun main() { + eprintln("x {}", 1) + eprint("y") + eprint("{:>3}\n", 7) + eprintln("{:?}", "q") + println("out") +} +"#; + let rust = r#" +fn main() { + eprintln!("x {}", 1); + eprint!("y"); + eprint!("{:>3}\n", 7); + eprintln!("{:?}", "q"); + println!("out"); +} +"#; + let dir = tempfile::tempdir().expect("tempdir"); + let reference = rustc_run(dir.path(), rust); + assert_eq!(reference.stdout, "out\n"); + assert_eq!(reference.stderr, "x 1\ny 7\n\"q\"\n"); + let got = output_of(ruchy_cmd(dir.path(), ruchy), "ruchy run"); + assert_eq!(got.stdout, reference.stdout, "ruchy stdout"); + assert_eq!(got.stderr, reference.stderr, "ruchy stderr"); +} + +// ---------------------------------------------------------------- FORMATFN-1 + +#[test] +fn test_formatfn_1_format_function_form_matches_macro() { + assert_matches_rust( + r#" +fun main() { + let s = format("{:?}", [1, 2]) + let t = format("{:>4}|{}", 7, "z") + println("{} {}", s, t) +} +"#, + r#" +fn main() { + let s = format!("{:?}", vec![1, 2]); + let t = format!("{:>4}|{}", 7, "z"); + println!("{} {}", s, t); +} +"#, + "[1, 2] 7|z\n", + ); +} + +// ---------------------------------------------------------------- DICTIDX-1 + +#[test] +fn test_dictidx_1_push_through_string_key() { + assert_matches_rust( + r#" +fun main() { + let mut d = {"k": [1]} + d["k"].push(2) + println("{:?}", d["k"]) +} +"#, + r#" +use std::collections::HashMap; +fn main() { + let mut d: HashMap<&str, Vec> = HashMap::new(); + d.insert("k", vec![1]); + d.get_mut("k").unwrap().push(2); + println!("{:?}", d["k"]); +} +"#, + "[1, 2]\n", + ); +} + +#[test] +fn test_dictidx_1_assign_and_compound_assign_string_key() { + assert_matches_rust( + r#" +fun main() { + let mut d = {"n": 1} + d["n"] = 5 + d["n"] += 1 + d["m"] = 10 + println("{} {}", d["n"], d["m"]) +} +"#, + r#" +use std::collections::HashMap; +fn main() { + let mut d: HashMap<&str, i64> = HashMap::new(); + d.insert("n", 1); + d.insert("n", 5); + *d.get_mut("n").unwrap() += 1; + d.insert("m", 10); + println!("{} {}", d["n"], d["m"]); +} +"#, + "6 10\n", + ); +} + +#[test] +fn test_dictidx_1_mixed_chains() { + assert_matches_rust( + r#" +fun main() { + let mut m = {"a": {"b": [1]}} + m["a"]["b"].push(3) + m["a"]["b"][0] += 40 + let mut v = [{"x": 1}] + v[0]["x"] += 10 + println("{:?} {}", m["a"]["b"], v[0]["x"]) +} +"#, + r#" +use std::collections::HashMap; +fn main() { + let mut inner: HashMap<&str, Vec> = HashMap::new(); + inner.insert("b", vec![1]); + let mut m: HashMap<&str, HashMap<&str, Vec>> = HashMap::new(); + m.insert("a", inner); + m.get_mut("a").unwrap().get_mut("b").unwrap().push(3); + m.get_mut("a").unwrap().get_mut("b").unwrap()[0] += 40; + let mut x: HashMap<&str, i64> = HashMap::new(); + x.insert("x", 1); + let mut v = vec![x]; + *v[0].get_mut("x").unwrap() += 10; + println!("{:?} {}", m["a"]["b"], v[0]["x"]); +} +"#, + "[41, 3] 11\n", + ); +} From 4f9aadc6bb65f8b748f991d1d05a56ce3098d4c1 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:41:24 +0200 Subject: [PATCH 080/106] RHLGA-1: close 6 G2b fixes in roadmap and CHANGELOG, file ARRVECARG/DICTTYPE/FLOATDISP/TMPLEAK - PRINTSTR-1, FMTSPEC-1, EPRINTLN-1, FORMATFN-1, RETLEN-1, DICTIDX-1 marked completed - New rows: ARRVECARG-1, DICTTYPE-1, FLOATDISP-1, TMPLEAK-1 (reproduced at 1d1e7594) - MAINUNIT-1 (already filed 2026-09-23) gains repro and acceptance criteria Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 15 ++++++ docs/roadmaps/roadmap.yaml | 104 ++++++++++++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c64c1cd63..de60f21e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,21 @@ Behaviour changes a program can observe are marked **(behaviour)**. with any placeholder (`{:?}`, `{:>5}`, `{0}`, …) uses it as the format string when compiled (PRINTLNFMT-1). Before, only `{}` was recognised, so `println("{:?}", v)` printed the format string followed by the value. +- **(behaviour)** A compiled `println(s)` of a single string-typed variable + prints the text, not its quoted `{:?}` form (PRINTSTR-1). +- `format("…", args)` transpiles to `format!` (FORMATFN-1), and an explicit + `return v.len()` from an `int` function is cast like a tail (RETLEN-1). +- A string key works at any link of an index chain: `d["k"]` reads, `d["k"] = v` + assigns and `d["k"].push(x)` mutates the map, in the interpreter and in + compiled code (DICTIDX-1). +- **(behaviour)** The interpreter's `println`, `print` and `format` share one + formatting engine that follows Rust's `std::fmt` (FMTSPEC-1): width, fill, + alignment, sign, `#`, `0`, precision and positional and named arguments. + Three results change: a placeholder count that does not match the argument + count is an error, `{:.2}` on an integer prints the integer (`42`), and + `{:.2}` on a string truncates it. +- `eprintln` and `eprint` are builtins that write formatted text to stderr + (EPRINTLN-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 58c7c6219..b676e8f04 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -3694,7 +3694,9 @@ roadmap: created: 2026-09-23T14:07:20Z updated: 2026-09-23T14:07:20Z spec: null - acceptance_criteria: [] + acceptance_criteria: + - 'Repro: fun hello() { println("hi") }; fun main() { let x = 1; hello() }' + - 'Acceptance: run and the compiled binary both print exactly hi' phases: [] subtasks: [] estimated_effort: null @@ -4403,11 +4405,11 @@ roadmap: github_issue: null item_type: task title: 'PRINTSTR-1: println(s) with a String/&str variable compiles to println!("{:?}", s) and prints quotes' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T11:30:00Z - updated: 2026-09-24T11:30:00Z + updated: 2026-09-24T14:00:00Z spec: null acceptance_criteria: - 'Repro: let t = s.to_string(); println(t) prints "hi" compiled, hi interpreted' @@ -4424,11 +4426,11 @@ roadmap: github_issue: null item_type: task title: 'FMTSPEC-1: interpreter println/format ignore width, precision and positional specs ({:>5}, {:.2}, {0})' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T11:30:00Z - updated: 2026-09-24T11:30:00Z + updated: 2026-09-24T14:00:00Z spec: null acceptance_criteria: - 'Repro: println("[{:>5}] [{:.2}]", 7, 3.14159) prints the placeholders literally' @@ -4445,11 +4447,11 @@ roadmap: github_issue: null item_type: task title: 'EPRINTLN-1: interpreter: eprintln/eprint are undefined functions' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T11:30:00Z - updated: 2026-09-24T11:30:00Z + updated: 2026-09-24T14:00:00Z spec: null acceptance_criteria: - 'Acceptance: eprintln/eprint write to stderr in the interpreter, as compiled' @@ -4465,11 +4467,11 @@ roadmap: github_issue: null item_type: task title: 'FORMATFN-1: format("{:?}", v) function form fails: rustc E0423 compiled, Unknown builtin __builtin_format__ interpreted' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T11:30:00Z - updated: 2026-09-24T11:30:00Z + updated: 2026-09-24T14:00:00Z spec: null acceptance_criteria: - 'Acceptance: format(...) function form equals format!(...) in both backends' @@ -4485,11 +4487,11 @@ roadmap: github_issue: null item_type: task title: 'RETLEN-1: return v.len() in a -> int function is not cast (E0308); INTLEN-1 covered only tails, lets and arguments' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T11:30:00Z - updated: 2026-09-24T11:30:00Z + updated: 2026-09-24T14:00:00Z spec: null acceptance_criteria: - 'Acceptance: rustc test for explicit return of len()/count() in int-returning functions' @@ -4505,11 +4507,11 @@ roadmap: github_issue: null item_type: task title: 'DICTIDX-1: string-keyed dict index: transpiler emits d["k" as usize]; interpreter d["k"].push(2) errors Array index must be an integer' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T11:30:00Z - updated: 2026-09-24T11:30:00Z + updated: 2026-09-24T14:00:00Z spec: null acceptance_criteria: - 'Repro: let mut d = {"k": [1]}; d["k"].push(2); println("{:?}", d["k"])' @@ -4522,3 +4524,79 @@ roadmap: - transpiler - filed-by:RHLGA-1 notes: null +- id: ARRVECARG-1 + github_issue: null + item_type: task + title: 'ARRVECARG-1: array literal passed to a Vec parameter transpiles without .to_vec() (E0308)' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T14:00:00Z + updated: 2026-09-24T14:00:00Z + spec: null + acceptance_criteria: + - 'Repro: fun n(v: Vec) -> i32 { v[0] }; fun main() { println("{}", n([1, 2, 3])) }' + - 'Acceptance: run prints 1; compile succeeds and the binary prints 1' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler +- id: DICTTYPE-1 + github_issue: null + item_type: task + title: 'DICTTYPE-1: dict literals with non-list values transpile as BTreeMap; d["k"] = 5 and d["a"] + d["b"] fail E0308' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T14:00:00Z + updated: 2026-09-24T14:00:00Z + spec: null + acceptance_criteria: + - 'Repro: let mut d = {"a": 1, "b": 2}; d["a"] = 5; println("{}", d["a"] + d["b"])' + - 'Acceptance: run and the compiled binary both print 7' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler +- id: FLOATDISP-1 + github_issue: null + item_type: task + title: 'FLOATDISP-1: interpreter prints an integral float with {} as 3.0 where the compiled binary prints 3' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T14:00:00Z + updated: 2026-09-24T14:00:00Z + spec: null + acceptance_criteria: + - 'Repro: let x = 3.0; println("{} {}", x, 2.5); println(x)' + - 'Acceptance: run and the compiled binary print identical text for {} and bare println of a float' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime +- id: TMPLEAK-1 + github_issue: null + item_type: task + title: 'TMPLEAK-1: ruchy_temp_bin_* binaries leak in the temp dir: notebook and property-tests handlers remove them only on the success path' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T14:00:00Z + updated: 2026-09-24T14:00:00Z + spec: null + acceptance_criteria: + - 'Repro: src/bin/handlers/property_tests_handler.rs:316-317 and notebook_handler.rs:32 return with ? before remove_file; 15 files (65M) found in /tmp' + - 'Acceptance: the temp binary is removed on every return path (RAII guard); a test asserts no ruchy_temp_bin_ file remains after a failing run' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - cli From bb96e7454c347522905ab5aeb83d1de48aee79fe Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:44:22 +0200 Subject: [PATCH 081/106] RHLGA-1: ARRVECARG-1 array literal argument to a Vec parameter becomes a Vec Adds tests/g2b_transfix_3.rs (RED for ARRVECARG-1, MAINUNIT-1, DICTTYPE-1); array literals passed to a parameter annotated Vec get .to_vec(). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 + docs/roadmaps/roadmap.yaml | 4 +- src/backend/transpiler/call_helpers.rs | 12 +- tests/g2b_transfix_3.rs | 194 +++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 tests/g2b_transfix_3.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index de60f21e8..9626a8dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,8 @@ Behaviour changes a program can observe are marked **(behaviour)**. `{:.2}` on a string truncates it. - `eprintln` and `eprint` are builtins that write formatted text to stderr (EPRINTLN-1). +- An array literal passed to a parameter annotated `Vec` becomes a `Vec` + in compiled code (ARRVECARG-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index b676e8f04..d6e01e51d 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4528,11 +4528,11 @@ roadmap: github_issue: null item_type: task title: 'ARRVECARG-1: array literal passed to a Vec parameter transpiles without .to_vec() (E0308)' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-24T14:00:00Z - updated: 2026-09-24T14:00:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: fun n(v: Vec) -> i32 { v[0] }; fun main() { println("{}", n([1, 2, 3])) }' diff --git a/src/backend/transpiler/call_helpers.rs b/src/backend/transpiler/call_helpers.rs index d44558a4f..4a06cbdd9 100644 --- a/src/backend/transpiler/call_helpers.rs +++ b/src/backend/transpiler/call_helpers.rs @@ -179,7 +179,10 @@ impl Transpiler { // BOOK-COMPAT-017: Array literal to Any parameter (function with inferred Vec type) // Convert array [1, 2, 3] to vec via .to_vec() for functions expecting Vec // But NOT when expected type is explicit array type [T; N] - (ExprKind::List(elements), "Any" | "Unknown") if !elements.is_empty() => { + // ARRVECARG-1: likewise for a parameter annotated `Vec` + (ExprKind::List(elements), expected) + if !elements.is_empty() && expects_vec_param(expected) => + { Ok(quote! { #tokens.to_vec() }) } // Array literal to explicit array type [T; N]: keep as-is @@ -206,6 +209,13 @@ impl Transpiler { } } +/// ARRVECARG-1: a parameter that takes an array literal as a `Vec`: an +/// inferred (`Any`/`Unknown`) parameter or one annotated `Vec`. +/// (complexity: 2) +fn expects_vec_param(expected: &str) -> bool { + matches!(expected, "Any" | "Unknown") || expected.starts_with("Vec<") +} + // ============================================================================ // Tests // ============================================================================ diff --git a/tests/g2b_transfix_3.rs b/tests/g2b_transfix_3.rs new file mode 100644 index 000000000..27cf490b5 --- /dev/null +++ b/tests/g2b_transfix_3.rs @@ -0,0 +1,194 @@ +//! RHLGA-1 transpiler fixes (third batch), each checked end to end: +//! `ruchy transpile`, `ruchy compile` (rustc), run the binary, compare exact +//! stdout, and require `ruchy run` to print the same. +//! +//! - ARRVECARG-1: an array literal argument to a `Vec` parameter becomes a +//! `Vec` (E0308 before). +//! - MAINUNIT-1: a call to a unit-returning user function as the last +//! statement of `main` is not wrapped in `println!("{:?}", ..)` (printed +//! `()` before); a value-returning tail is still printed (BOOK-COMPAT-015). +//! - DICTTYPE-1: a dict literal whose values are all ints, floats or bools is +//! typed by those values, not stringified to `BTreeMap`. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +/// (stdout, stderr) of a command that must succeed. +fn output_of(mut cmd: Command, what: &str) -> (String, String) { + let out = cmd.output().expect("spawn command"); + let stdout = String::from_utf8(out.stdout).expect("utf8 stdout"); + let stderr = String::from_utf8(out.stderr).expect("utf8 stderr"); + assert!(out.status.success(), "{what} failed:\n{stdout}{stderr}"); + (stdout, stderr) +} + +fn transpile(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + output_of(cmd, &format!("ruchy transpile of:\n{src}\n")).0 +} + +/// `ruchy compile` the source, run the binary, return (stdout, stderr). +fn compile_and_run(src: &str) -> (String, String) { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let bin = dir.path().join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(90)); + output_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + output_of(run, "compiled binary") +} + +fn interpret(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + output_of(cmd, &format!("ruchy run of:\n{src}\n")).0 +} + +fn main_with(body: &str) -> String { + format!("fun main() {{\n{body}\n}}\n") +} + +/// Compiled stdout is `expected`; with `interp`, `ruchy run` agrees. +fn check(src: &str, expected: &str, interp: bool) { + let (stdout, _) = compile_and_run(src); + assert_eq!( + stdout, + expected, + "compiled stdout of:\n{src}\n{}", + transpile(src) + ); + if interp { + assert_eq!(interpret(src), expected, "interpreter stdout of:\n{src}"); + } +} + +// --------------------------------------------------------------- ARRVECARG-1 + +#[test] +fn test_arrvecarg_1_array_literal_to_vec_param() { + let src = "fun n(v: Vec) -> i32 {\n v[0]\n}\n\ + fun main() {\n println(\"{}\", n([1, 2, 3]))\n}\n"; + check(src, "1\n", true); +} + +#[test] +fn test_arrvecarg_1_vec_param_iterated() { + let src = "fun total(v: Vec) -> i32 {\n let mut s = 0\n for x in v {\n s = s + x\n }\n s\n}\n\ + fun main() {\n println(\"{}\", total([4, 5, 6]))\n}\n"; + check(src, "15\n", true); +} + +#[test] +fn test_arrvecarg_1_second_vec_param() { + let src = "fun pick(i: i32, v: Vec) -> i32 {\n v[i]\n}\n\ + fun main() {\n println(\"{}\", pick(2, [7, 8, 9]))\n}\n"; + check(src, "9\n", true); +} + +// ---------------------------------------------------------------- MAINUNIT-1 + +#[test] +fn test_mainunit_1_unit_call_after_let() { + let src = "fun hello() {\n println(\"hi\")\n}\n\ + fun main() {\n let x = 1\n hello()\n}\n"; + check(src, "hi\n", true); +} + +#[test] +fn test_mainunit_1_unit_call_only_statement() { + let src = "fun hello() {\n println(\"hi\")\n}\n\ + fun main() {\n hello()\n}\n"; + check(src, "hi\n", true); +} + +#[test] +fn test_mainunit_1_unit_function_with_loop() { + let src = "fun count(n: i32) {\n for i in 0..n {\n println(\"{}\", i)\n }\n}\n\ + fun main() {\n println(\"start\")\n count(2)\n}\n"; + check(src, "start\n0\n1\n", true); +} + +#[test] +fn test_mainunit_1_value_tail_still_printed() { + // BOOK-COMPAT-015: a value-returning tail call of main is printed. + let src = "fun add(a: i32, b: i32) -> i32 {\n a + b\n}\n\ + fun main() {\n let x = 1\n add(x, 2)\n}\n"; + let (stdout, _) = compile_and_run(src); + assert_eq!(stdout, "3\n", "compiled stdout of:\n{}", transpile(src)); +} + +// ---------------------------------------------------------------- DICTTYPE-1 + +#[test] +fn test_dicttype_1_int_values_assign_and_add() { + let src = main_with( + r#" let mut d = {"a": 1, "b": 2} + d["a"] = 5 + println("{}", d["a"] + d["b"])"#, + ); + check(&src, "7\n", true); +} + +#[test] +fn test_dicttype_1_float_values() { + let src = main_with( + r#" let d = {"x": 1.5, "y": 2.25} + println("{}", d["x"] + d["y"])"#, + ); + check(&src, "3.75\n", true); +} + +#[test] +fn test_dicttype_1_bool_values() { + let src = main_with( + r#" let mut d = {"on": true, "off": false} + d["off"] = true + println("{}", d["on"] && d["off"])"#, + ); + check(&src, "true\n", true); +} + +#[test] +fn test_dicttype_1_string_values_unchanged() { + let src = main_with( + r#" let d = {"a": "x", "b": "y"} + println("{}", d["a"])"#, + ); + check(&src, "x\n", true); +} + +#[test] +fn test_dicttype_1_int_values_emit_no_string_map() { + let out = transpile(&main_with( + r#" let d = {"a": 1, "b": 2} + println("{}", d["a"])"#, + )); + assert!( + !out.contains("BTreeMap < String , String >") && !out.contains("BTreeMap"), + "int-valued dict must not be a String map:\n{out}" + ); +} From cb6016b428a8cacfe5569e17571c319871ae4a1e Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:47:30 +0200 Subject: [PATCH 082/106] RHLGA-1: MAINUNIT-1 a unit-returning call ending main is not printed as () Signature collection records user functions with no declared return type and a void body; has_non_unit_last_expr treats a call to one as unit. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 ++ docs/roadmaps/roadmap.yaml | 4 +-- src/backend/transpiler/ast_analysis.rs | 10 +++++- src/backend/transpiler/function_transpiler.rs | 31 ++++++++++++------- src/backend/transpiler/mod.rs | 5 +++ 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9626a8dc2..69981c20c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,6 +122,9 @@ Behaviour changes a program can observe are marked **(behaviour)**. (EPRINTLN-1). - An array literal passed to a parameter annotated `Vec` becomes a `Vec` in compiled code (ARRVECARG-1). +- A call to a unit-returning user function as the last statement of `main` + is no longer wrapped in `println!("{:?}", ..)`, so the compiled program does + not print `()` (MAINUNIT-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index d6e01e51d..e88a26e4d 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -3688,11 +3688,11 @@ roadmap: github_issue: null item_type: task title: 'MAINUNIT-1: a unit-returning call as the last statement of main is transpiled as println!("{:?}", f()) and prints ()' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-23T14:07:20Z - updated: 2026-09-23T14:07:20Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: fun hello() { println("hi") }; fun main() { let x = 1; hello() }' diff --git a/src/backend/transpiler/ast_analysis.rs b/src/backend/transpiler/ast_analysis.rs index 66e970bc7..fe7f5ae8b 100644 --- a/src/backend/transpiler/ast_analysis.rs +++ b/src/backend/transpiler/ast_analysis.rs @@ -219,8 +219,16 @@ impl Transpiler { pub fn collect_signatures_from_expr(&mut self, expr: &Expr) { match &expr.kind { ExprKind::Function { - name, params, body, .. + name, + params, + body, + return_type, + .. } => { + // MAINUNIT-1: remember functions whose calls yield `()` + if return_type.is_none() && super::function_analysis::is_void_expression(body) { + self.unit_functions.insert(name.clone()); + } let param_types: Vec = params .iter() .map(|param| Self::type_to_string(¶m.ty)) diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index 05a2a14c7..c740813bd 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -89,7 +89,7 @@ impl Transpiler { } else { self.generate_body_tokens(body, is_async)? } - } else if name == "main" && Self::has_non_unit_last_expr(body) { + } else if name == "main" && self.has_non_unit_last_expr(body) { // BOOK-COMPAT-015: Main functions that end with a non-unit expression // should print it instead of trying to return it self.generate_main_body_with_print(body, is_async)? @@ -124,22 +124,29 @@ impl Transpiler { } /// BOOK-COMPAT-015: Check if body has a non-unit last expression - fn has_non_unit_last_expr(body: &Expr) -> bool { + /// (MAINUNIT-1: a call to a unit-returning user function is unit) + fn has_non_unit_last_expr(&self, body: &Expr) -> bool { match &body.kind { - ExprKind::Block(exprs) => { - if let Some(last) = exprs.last() { - Self::has_non_unit_last_expr(last) - } else { - false - } - } + ExprKind::Block(exprs) => exprs + .last() + .is_some_and(|last| self.has_non_unit_last_expr(last)), ExprKind::Let { body, .. } | ExprKind::LetPattern { body, .. } => { - Self::has_non_unit_last_expr(body) + self.has_non_unit_last_expr(body) } - _ => !Self::is_unit_expr(body), + _ => !Self::is_unit_expr(body) && !self.is_unit_function_call(body), } } + /// MAINUNIT-1: a call to a user function recorded as returning `()`. + /// (complexity: 2) + fn is_unit_function_call(&self, expr: &Expr) -> bool { + matches!( + &expr.kind, + ExprKind::Call { func, .. } + if matches!(&func.kind, ExprKind::Identifier(name) if self.unit_functions.contains(name)) + ) + } + /// Check if expression is a unit expression (doesn't return a value) fn is_unit_expr(expr: &Expr) -> bool { use crate::frontend::ast::Literal; @@ -252,7 +259,7 @@ impl Transpiler { }, None => { let run = quote! { __ruchy_block_on(async move { #inner }) }; - if Self::has_non_unit_last_expr(body) { + if self.has_non_unit_last_expr(body) { quote! { let __ruchy_main_value = #run; println!("{:?}", __ruchy_main_value); } } else { quote! { #run; } diff --git a/src/backend/transpiler/mod.rs b/src/backend/transpiler/mod.rs index 96363d21d..237d8dd4b 100644 --- a/src/backend/transpiler/mod.rs +++ b/src/backend/transpiler/mod.rs @@ -177,6 +177,9 @@ pub struct Transpiler { /// /// Maps function names to their parameter types for proper type conversion. pub function_signatures: std::collections::HashMap, + /// MAINUNIT-1: user functions with no declared return type whose body is + /// void, so a call to one yields `()` (not printed as `main`'s tail). + pub unit_functions: std::collections::HashSet, /// Module names that have been imported/defined (Issue #103). /// /// Tracks module identifiers so field access can use :: syntax for module paths. @@ -240,6 +243,7 @@ impl Clone for Transpiler { in_loop_context: std::cell::Cell::new(self.in_loop_context.get()), mutable_vars: self.mutable_vars.clone(), function_signatures: self.function_signatures.clone(), + unit_functions: self.unit_functions.clone(), module_names: self.module_names.clone(), string_vars: std::cell::RefCell::new(self.string_vars.borrow().clone()), current_function_return_type: std::cell::RefCell::new( @@ -282,6 +286,7 @@ impl Transpiler { in_loop_context: std::cell::Cell::new(false), mutable_vars: std::collections::HashSet::new(), function_signatures: std::collections::HashMap::new(), + unit_functions: std::collections::HashSet::new(), module_names: std::collections::HashSet::new(), string_vars: std::cell::RefCell::new(std::collections::HashSet::new()), current_function_return_type: std::cell::RefCell::new(None), From 3d46750b197fb526170001b3efc91d0462d08a5d Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:54:59 +0200 Subject: [PATCH 083/106] RHLGA-1: DICTTYPE-1 int, float and bool dict literals keep their value type The DICTIDX-1 list-valued dict path now covers any dict whose values share one typed class (lists, ints, floats, bools); a binding to one is recorded so d["k"] = v inserts the value unconverted. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 + docs/roadmaps/roadmap.yaml | 4 +- src/backend/transpiler/expressions.rs | 3 +- .../expressions_helpers/collections.rs | 86 ++++++++++++++++--- src/backend/transpiler/print_helpers.rs | 4 +- 5 files changed, 85 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69981c20c..3ce0f30c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. - A call to a unit-returning user function as the last statement of `main` is no longer wrapped in `println!("{:?}", ..)`, so the compiled program does not print `()` (MAINUNIT-1). +- A dict literal whose values are all ints, all floats or all bools keeps that + value type in compiled code instead of `BTreeMap`, so + `d["a"] = 5` and `d["a"] + d["b"]` compile; string and mixed dicts are + unchanged (DICTTYPE-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index e88a26e4d..05c0277b6 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4547,11 +4547,11 @@ roadmap: github_issue: null item_type: task title: 'DICTTYPE-1: dict literals with non-list values transpile as BTreeMap; d["k"] = 5 and d["a"] + d["b"] fail E0308' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T14:00:00Z - updated: 2026-09-24T14:00:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let mut d = {"a": 1, "b": 2}; d["a"] = 5; println("{}", d["a"] + d["b"])' diff --git a/src/backend/transpiler/expressions.rs b/src/backend/transpiler/expressions.rs index 61a3c0475..fa7ec65bf 100644 --- a/src/backend/transpiler/expressions.rs +++ b/src/backend/transpiler/expressions.rs @@ -172,7 +172,8 @@ impl Transpiler { } let obj_tokens = self.transpile_place(object)?; let key_tokens = self.transpile_expr(index)?; - let value_tokens = self.map_value_tokens(value, false)?; + // DICTTYPE-1: a typed-value dict stores the value as is + let value_tokens = self.map_value_tokens(value, self.is_typed_dict(object))?; Ok(Some( quote! { #obj_tokens.insert(#key_tokens.to_string(), #value_tokens) }, )) diff --git a/src/backend/transpiler/expressions_helpers/collections.rs b/src/backend/transpiler/expressions_helpers/collections.rs index 8d48ae420..57721787b 100644 --- a/src/backend/transpiler/expressions_helpers/collections.rs +++ b/src/backend/transpiler/expressions_helpers/collections.rs @@ -149,9 +149,10 @@ impl Transpiler { &self, fields: &[crate::frontend::ast::ObjectField], ) -> Result { - // DICTIDX-1: a dict whose values are all lists maps to `Vec`s - if list_valued_fields(fields) { - return self.transpile_list_valued_object(fields); + // DICTIDX-1 / DICTTYPE-1: a dict whose values are all lists, ints, + // floats or bools keeps its value type + if typed_value_fields(fields) { + return self.transpile_typed_value_object(fields); } let field_tokens = self.collect_hashmap_field_tokens(fields)?; // DEFECT-DICT-DETERMINISM FIX: Use BTreeMap for deterministic key ordering @@ -165,8 +166,9 @@ impl Transpiler { }) } /// DICTIDX-1: `{"k": [..], ..}` is a `BTreeMap>`, so - /// `d["k"].push(x)` and `{:?}` work on the real list. (complexity: 3) - fn transpile_list_valued_object( + /// `d["k"].push(x)` and `{:?}` work on the real list; DICTTYPE-1: likewise + /// `{"k": 1, ..}` is a map of ints (floats, bools). (complexity: 3) + fn transpile_typed_value_object( &self, fields: &[crate::frontend::ast::ObjectField], ) -> Result { @@ -943,12 +945,72 @@ mod tests { } } -/// DICTIDX-1: a non-empty dict literal whose values are all list literals -/// (no spread). (complexity: 3) -fn list_valued_fields(fields: &[crate::frontend::ast::ObjectField]) -> bool { +/// DICTTYPE-1: `variable_types` record of a binding to a dict literal whose +/// values keep their type (see [`typed_value_fields`]). +pub(crate) const TYPED_DICT_VAR_TYPE: &str = "__ruchy_typed_dict"; + +/// DICTTYPE-1: the class of a dict value that keeps its own type in the map. +#[derive(Clone, Copy, PartialEq, Eq)] +enum DictValueKind { + List, + Int, + Float, + Bool, +} + +/// DICTTYPE-1: the typed class of a dict value, if it has one: a list +/// literal, an int, float or bool literal, or a negated number. +/// (complexity: 6) +fn dict_value_kind(value: &Expr) -> Option { + use crate::frontend::ast::UnaryOp; + match &value.kind { + ExprKind::List(_) => Some(DictValueKind::List), + ExprKind::Literal(Literal::Integer(..)) => Some(DictValueKind::Int), + ExprKind::Literal(Literal::Float(_)) => Some(DictValueKind::Float), + ExprKind::Literal(Literal::Bool(_)) => Some(DictValueKind::Bool), + ExprKind::Unary { + op: UnaryOp::Negate, + operand, + } => dict_value_kind(operand) + .filter(|k| matches!(k, DictValueKind::Int | DictValueKind::Float)), + _ => None, + } +} + +/// DICTIDX-1 / DICTTYPE-1: a non-empty dict literal (no spread) whose values +/// are all of one typed class: all lists, all ints, all floats or all bools. +/// Other dicts (strings, mixed) stay `BTreeMap`. +/// (complexity: 4) +fn typed_value_fields(fields: &[crate::frontend::ast::ObjectField]) -> bool { use crate::frontend::ast::ObjectField; - !fields.is_empty() - && fields.iter().all(|f| { - matches!(f, ObjectField::KeyValue { value, .. } if matches!(value.kind, ExprKind::List(_))) - }) + let mut kinds = fields.iter().map(|f| match f { + ObjectField::KeyValue { value, .. } => dict_value_kind(value), + ObjectField::Spread { .. } => None, + }); + match kinds.next() { + Some(Some(first)) => kinds.all(|k| k == Some(first)), + _ => false, + } +} + +impl Transpiler { + /// DICTTYPE-1: record whether the binding `name` holds a typed-value dict + /// literal; a rebinding to anything else clears the record. (complexity: 3) + pub(crate) fn track_typed_dict_binding(&self, name: &str, value: &Expr) { + let typed = + matches!(&value.kind, ExprKind::ObjectLiteral { fields } if typed_value_fields(fields)); + let mut types = self.variable_types.borrow_mut(); + if typed { + types.insert(name.to_string(), TYPED_DICT_VAR_TYPE.to_string()); + } else if types.get(name).is_some_and(|t| t == TYPED_DICT_VAR_TYPE) { + types.remove(name); + } + } + + /// DICTTYPE-1: `expr` names a binding recorded as a typed-value dict. + /// (complexity: 2) + pub(crate) fn is_typed_dict(&self, expr: &Expr) -> bool { + matches!(&expr.kind, ExprKind::Identifier(name) + if self.variable_types.borrow().get(name).is_some_and(|t| t == TYPED_DICT_VAR_TYPE)) + } } diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index 16c91fdd0..97ecbd353 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -290,10 +290,12 @@ impl Transpiler { /// PRINTSTR-1: record whether the binding `name` holds a string, from its /// annotation when present, else from its value. A rebinding to a - /// non-string clears the record. (complexity: 2) + /// non-string clears the record. DICTTYPE-1: a typed-value dict literal + /// is recorded too. (complexity: 2) pub(crate) fn track_string_binding(&self, name: &str, value: &Expr, ty: Option<&Type>) { let is_string = ty.map_or_else(|| self.is_display_string(value), is_string_annotation); self.set_string_var(name, is_string); + self.track_typed_dict_binding(name, value); } /// PRINTSTR-1: parameters of the function being transpiled; string From e8d6b626ed5efeb2a21e428ea72191144e4ba2af Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:51:03 +0200 Subject: [PATCH 084/106] RHLGA-1: FLOATDISP-1 interpreter {} of a float prints Rust's f64 Display - fmt_spec: {} of a float uses display_float (format!("{f}")): 3.0 -> 3, 1e21 -> 1000000000000000000000, -0.0 -> -0; {:?} keeps Rust Debug. - A bare println(x) of a float keeps Debug text (3.0, 1e21), as the transpiler emits {:?}; f-string {x} uses Display like format!("{}"). - tests/g2b_runfix_3.rs: 7 FLOATDISP-1 tests against rustc references and the ruchy-compiled binary. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 5 + docs/roadmaps/roadmap.yaml | 4 +- src/runtime/eval_string_interpolation.rs | 22 ++- src/runtime/fmt_spec.rs | 25 ++- tests/g2b_runfix_3.rs | 235 +++++++++++++++++++++++ 5 files changed, 274 insertions(+), 17 deletions(-) create mode 100644 tests/g2b_runfix_3.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce0f30c0..63497dfcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -129,6 +129,11 @@ Behaviour changes a program can observe are marked **(behaviour)**. value type in compiled code instead of `BTreeMap`, so `d["a"] = 5` and `d["a"] + d["b"]` compile; string and mixed dicts are unchanged (DICTTYPE-1). +- **(behaviour)** The interpreter prints a float under `{}` (in `println`, + `format` and f-strings) as Rust's f64 Display does: `3`, not `3.0`; + `1000000000000000000000`, `-0`, `NaN`, `inf` (FLOATDISP-1). `{:?}` and a + bare `println(x)`, which compiles to `{:?}`, print Rust's Debug text + (`3.0`, `1e21`, `1e-7`), matching the compiled binary. - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 05c0277b6..7882c2b8b 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4566,11 +4566,11 @@ roadmap: github_issue: null item_type: task title: 'FLOATDISP-1: interpreter prints an integral float with {} as 3.0 where the compiled binary prints 3' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-24T14:00:00Z - updated: 2026-09-24T14:00:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let x = 3.0; println("{} {}", x, 2.5); println(x)' diff --git a/src/runtime/eval_string_interpolation.rs b/src/runtime/eval_string_interpolation.rs index f22965141..7ffe52436 100644 --- a/src/runtime/eval_string_interpolation.rs +++ b/src/runtime/eval_string_interpolation.rs @@ -99,27 +99,31 @@ pub fn format_value_with_spec(value: &Value, format_spec: &str) -> String { } /// Format a value for string interpolation (no extra quotes for strings) -/// -/// # Complexity -/// Cyclomatic complexity: 8 (within Toyota Way limits) pub fn format_value_for_interpolation(value: &Value) -> String { + match value { + // FLOATDISP-1: `f"{x}"` transpiles to `{}`, Rust's f64 Display. + Value::Float(f) => crate::runtime::fmt_spec::display_float(*f), + _ => interpolation_text(value), + } +} + +/// Interpolation text of a non-float value; compound elements keep the +/// value's own text. +fn interpolation_text(value: &Value) -> String { match value { Value::String(s) => s.to_string(), // No quotes for interpolation Value::Array(arr) => { - let elements: Vec = arr.iter().map(format_value_for_interpolation).collect(); + let elements: Vec = arr.iter().map(interpolation_text).collect(); format!("[{}]", elements.join(", ")) } Value::Tuple(elements) => { - let formatted: Vec = elements - .iter() - .map(format_value_for_interpolation) - .collect(); + let formatted: Vec = elements.iter().map(interpolation_text).collect(); format!("({})", formatted.join(", ")) } Value::Object(map) => { let mut entries = Vec::new(); for (k, v) in map.iter() { - entries.push(format!("{}: {}", k, format_value_for_interpolation(v))); + entries.push(format!("{}: {}", k, interpolation_text(v))); } format!("{{{}}}", entries.join(", ")) } diff --git a/src/runtime/fmt_spec.rs b/src/runtime/fmt_spec.rs index 43343871e..b902d1e0a 100644 --- a/src/runtime/fmt_spec.rs +++ b/src/runtime/fmt_spec.rs @@ -348,11 +348,24 @@ pub fn format_call( } fn join_display(values: &[Value]) -> String { - values - .iter() - .map(format_value_display) - .collect::>() - .join(" ") + values.iter().map(bare_text).collect::>().join(" ") +} + +/// FLOATDISP-1: text of a bare `println(v)` argument. The transpiler emits +/// `{:?}` for a non-string argument, so a float keeps Rust's Debug text +/// (`3.0`, `1e21`). +fn bare_text(value: &Value) -> String { + match value { + Value::Float(_) => rust_debug(value), + _ => format_value_display(value), + } +} + +/// FLOATDISP-1: a float under `{}`, as Rust's `impl Display for f64` writes +/// it (`3`, `2.5`, `1000000000000000000000`, `NaN`, `inf`, `-0`). +#[must_use] +pub fn display_float(f: f64) -> String { + format!("{f}") } /// Format one value under a spec. @@ -445,7 +458,7 @@ fn float_digits(f: f64, spec: &Spec) -> Result { (Kind::UpperExp, Some(p)) => format!("{a:.p$E}"), (Kind::UpperExp, None) => format!("{a:E}"), (Kind::Display | Kind::Debug, Some(p)) => format!("{a:.p$}"), - (Kind::Display, None) => format_value_display(&Value::Float(a)), + (Kind::Display, None) => display_float(a), (Kind::Debug, None) => rust_debug(&Value::Float(a)), (kind, _) => { return Err(format!( diff --git a/tests/g2b_runfix_3.rs b/tests/g2b_runfix_3.rs new file mode 100644 index 000000000..0a9039217 --- /dev/null +++ b/tests/g2b_runfix_3.rs @@ -0,0 +1,235 @@ +//! RHLGA-1 G2b run fixes, part 3. +//! +//! - FLOATDISP-1: `ruchy run` printed a float under `{}` with Debug text +//! (`3.0`, `1000000000000000000000.0`, `-0.0`); Rust's f64 Display prints +//! `3`, `1000000000000000000000`, `-0`. A bare `println(x)` transpiles to +//! `println!("{:?}", x)`, so it keeps Rust's Debug text (`3.0`, `1e21`). +//! - TMPLEAK-1: `property-tests`, `fuzz` and `notebook` removed the compiled +//! `ruchy_temp_bin_*` binary only on the success path; an error returned +//! with `?` left it in the temp dir. +//! +//! Each FLOATDISP case states the output Rust prints for the same program; a +//! hand-written Rust reference is compiled with rustc to prove it. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn ruchy() -> Command { + Command::cargo_bin("ruchy").expect("ruchy binary is built for tests") +} + +fn ruchy_run(dir: &Path, src: &str) -> String { + let mut cmd = ruchy(); + cmd.arg("run") + .arg(write_source(dir, src)) + .timeout(Duration::from_secs(10)); + stdout_of(cmd, "ruchy run") +} + +fn rustc_run(dir: &Path, rust: &str) -> String { + let main_rs = dir.join("reference.rs"); + let bin = dir.join("reference_bin"); + std::fs::write(&main_rs, rust).expect("write reference.rs"); + let mut rustc = Command::new("rustc"); + rustc + .args(["--edition", "2021", "-A", "warnings", "-o"]) + .arg(&bin) + .arg(&main_rs) + .timeout(Duration::from_secs(120)); + stdout_of(rustc, "rustc on the Rust reference"); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(10)); + stdout_of(run, "Rust reference binary") +} + +/// Rust prints `expected` for `rust`, and `ruchy run` prints it for `ruchy`. +fn assert_matches_rust(ruchy_src: &str, rust: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!(rustc_run(dir.path(), rust), expected, "Rust reference"); + assert_eq!(ruchy_run(dir.path(), ruchy_src), expected, "ruchy run"); +} + +// ------------------------------------------------------------- FLOATDISP-1 + +const BRIEF_CASE: &str = r#" +fun main() { + let x = 3.0 + println("{} {}", x, 2.5) + println(x) +} +"#; + +#[test] +fn test_floatdisp_1_brief_case_display_and_bare_println() { + assert_matches_rust( + BRIEF_CASE, + r#" +fn main() { + let x = 3.0f64; + println!("{} {}", x, 2.5); + println!("{:?}", x); +} +"#, + "3 2.5\n3.0\n", + ); +} + +#[test] +fn test_floatdisp_1_display_special_values() { + assert_matches_rust( + r#" +fun main() { + let a = 1e21 + let b = 0.0 / 0.0 + let c = 1.0 / 0.0 + let d = -0.0 + let e = 1e-7 + let g = 0.1 + 0.2 + println("{} {} {} {} {} {} {}", a, b, c, d, e, g, -c) +} +"#, + r#" +fn main() { + let a = 1e21f64; + let b = 0.0f64 / 0.0; + let c = 1.0f64 / 0.0; + let d = -0.0f64; + let e = 1e-7f64; + let g = 0.1f64 + 0.2; + println!("{} {} {} {} {} {} {}", a, b, c, d, e, g, -c); +} +"#, + "1000000000000000000000 NaN inf -0 0.0000001 0.30000000000000004 -inf\n", + ); +} + +#[test] +fn test_floatdisp_1_debug_special_values_unchanged() { + assert_matches_rust( + r#" +fun main() { + let a = 1e21 + let d = -0.0 + let e = 1e-7 + println("{:?} {:?} {:?} {:?} {:?}", 3.0, a, d, e, 2.5) +} +"#, + r#" +fn main() { + let a = 1e21f64; + let d = -0.0f64; + let e = 1e-7f64; + println!("{:?} {:?} {:?} {:?} {:?}", 3.0f64, a, d, e, 2.5f64); +} +"#, + "3.0 1e21 -0.0 1e-7 2.5\n", + ); +} + +#[test] +fn test_floatdisp_1_bare_println_is_debug() { + assert_matches_rust( + r#" +fun main() { + let a = 1e21 + let e = 1e-7 + let d = -0.0 + println(a) + println(e) + println(d) + println(3.0) +} +"#, + r#" +fn main() { + let a = 1e21f64; + let e = 1e-7f64; + let d = -0.0f64; + println!("{:?}", a); + println!("{:?}", e); + println!("{:?}", d); + println!("{:?}", 3f64); +} +"#, + "1e21\n1e-7\n-0.0\n3.0\n", + ); +} + +#[test] +fn test_floatdisp_1_width_sign_and_alignment() { + assert_matches_rust( + r#" +fun main() { + let x = 3.0 + println("[{:>5}] [{:+}] [{:<4}] [{:^5}] [{:05}]", x, x, x, x, -x) +} +"#, + r#" +fn main() { + let x = 3.0f64; + println!("[{:>5}] [{:+}] [{:<4}] [{:^5}] [{:05}]", x, x, x, x, -x); +} +"#, + "[ 3] [+3] [3 ] [ 3 ] [-0003]\n", + ); +} + +#[test] +fn test_floatdisp_1_format_fn_and_fstring() { + assert_matches_rust( + r#" +fun main() { + let x = 3.0 + println(format("{}", x)) + println(f"{x} {x:?} {x:>4}") + println(x.to_string()) +} +"#, + r#" +fn main() { + let x = 3.0f64; + println!("{}", format!("{}", x)); + println!("{} {:?} {:>4}", x, x, x); + println!("{}", x.to_string()); +} +"#, + "3\n3 3.0 3\n3\n", + ); +} + +#[test] +fn test_floatdisp_1_run_matches_compiled_binary() { + let dir = tempfile::tempdir().expect("tempdir"); + let src = write_source(dir.path(), BRIEF_CASE); + let bin = dir.path().join("prog_bin"); + let mut compile = ruchy(); + compile + .arg("compile") + .arg(&src) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(120)); + stdout_of(compile, "ruchy compile"); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(10)); + let compiled = stdout_of(run, "compiled binary"); + assert_eq!(compiled, "3 2.5\n3.0\n", "compiled binary"); + assert_eq!(ruchy_run(dir.path(), BRIEF_CASE), compiled, "ruchy run"); +} From 316b7433d45ebc3eae9b5d3cc76e1f3dcf507ddd Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 11:52:06 +0200 Subject: [PATCH 085/106] RHLGA-1: TMPLEAK-1 temp binaries are removed on every return path - prepare_compilation returns the binary path as a tempfile::TempPath, so the compiled ruchy_temp_bin_* is removed when it drops, including an early return through ?. - compile_for_property_testing returns the TempPath; property-tests, fuzz and notebook drop it instead of calling remove_file on the success path. - tests/g2b_runfix_3.rs: 4 TMPLEAK-1 tests run failing commands with a fresh TMPDIR and assert no ruchy_temp_bin_ file remains. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 + docs/roadmaps/roadmap.yaml | 4 +- src/bin/handlers/fuzz_handler.rs | 2 +- src/bin/handlers/notebook_handler.rs | 7 +-- src/bin/handlers/property_tests_handler.rs | 12 ++-- src/bin/handlers/run_handler.rs | 9 ++- tests/g2b_runfix_3.rs | 73 ++++++++++++++++++++++ 7 files changed, 96 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63497dfcb..e9f03cb19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,6 +134,9 @@ Behaviour changes a program can observe are marked **(behaviour)**. `1000000000000000000000`, `-0`, `NaN`, `inf` (FLOATDISP-1). `{:?}` and a bare `println(x)`, which compiles to `{:?}`, print Rust's Debug text (`3.0`, `1e21`, `1e-7`), matching the compiled binary. +- `property-tests`, `fuzz` and `notebook` remove the compiled + `ruchy_temp_bin_*` binary on every return path, not only on success + (TMPLEAK-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 7882c2b8b..3d8c6468e 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4585,11 +4585,11 @@ roadmap: github_issue: null item_type: task title: 'TMPLEAK-1: ruchy_temp_bin_* binaries leak in the temp dir: notebook and property-tests handlers remove them only on the success path' - status: planned + status: completed priority: medium assigned_to: null created: 2026-09-24T14:00:00Z - updated: 2026-09-24T14:00:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: src/bin/handlers/property_tests_handler.rs:316-317 and notebook_handler.rs:32 return with ? before remove_file; 15 files (65M) found in /tmp' diff --git a/src/bin/handlers/fuzz_handler.rs b/src/bin/handlers/fuzz_handler.rs index 1f05027bf..32a74c959 100644 --- a/src/bin/handlers/fuzz_handler.rs +++ b/src/bin/handlers/fuzz_handler.rs @@ -258,7 +258,7 @@ fn handle_fuzz_single_file( let (successes, crashes, timeouts, crash_details) = run_fuzz_iterations(&binary_path, iterations, verbose)?; - let _ = fs::remove_file(&binary_path); + drop(binary_path); let total = successes + crashes + timeouts; let success_rate = (successes as f64 / total as f64) * 100.0; diff --git a/src/bin/handlers/notebook_handler.rs b/src/bin/handlers/notebook_handler.rs index fa4c17301..1c4a67fd6 100644 --- a/src/bin/handlers/notebook_handler.rs +++ b/src/bin/handlers/notebook_handler.rs @@ -17,7 +17,6 @@ fn validate_notebook_file(path: &Path) -> Result<()> { compile_rust_code, parse_source, prepare_compilation, read_file_with_context, transpile_for_execution, }; - use std::fs; println!("📓 Notebook validation mode for: {}", path.display()); @@ -28,11 +27,9 @@ fn validate_notebook_file(path: &Path) -> Result<()> { let (temp_source, binary_path) = prepare_compilation(&rust_code, false)?; compile_rust_code(temp_source.path(), &binary_path)?; - // Execute the file to validate it runs + // Execute the file to validate it runs; the binary drops on every path. let result = std::process::Command::new(&binary_path).output()?; - - // Cleanup - let _ = fs::remove_file(&binary_path); + drop(binary_path); if result.status.success() { println!("✅ Notebook validation: PASSED"); diff --git a/src/bin/handlers/property_tests_handler.rs b/src/bin/handlers/property_tests_handler.rs index c02ecc6ac..288821622 100644 --- a/src/bin/handlers/property_tests_handler.rs +++ b/src/bin/handlers/property_tests_handler.rs @@ -4,7 +4,7 @@ use anyhow::Result; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; /// Run property test suite via cargo test /// Complexity: 3 (Toyota Way: <10) @@ -106,7 +106,12 @@ pub fn handle_property_tests_command( } /// Compile Ruchy file for property testing -pub(crate) fn compile_for_property_testing(path: &Path, verbose: bool) -> Result { +/// +/// TMPLEAK-1: the binary is removed when the returned path drops. +pub(crate) fn compile_for_property_testing( + path: &Path, + verbose: bool, +) -> Result { use super::{compile_rust_code, prepare_compilation, transpile_for_execution}; use crate::handlers::transpile_handler::parse_source; @@ -322,8 +327,7 @@ fn handle_property_tests_single_file( if !deterministic { test_results.push("Determinism test: FAILED - outputs differ".to_string()); } - - let _ = fs::remove_file(&binary_path); + drop(binary_path); generate_property_test_report( path, diff --git a/src/bin/handlers/run_handler.rs b/src/bin/handlers/run_handler.rs index 2cce514b7..429c1781f 100644 --- a/src/bin/handlers/run_handler.rs +++ b/src/bin/handlers/run_handler.rs @@ -6,7 +6,7 @@ use anyhow::{Context, Result}; use ruchy::frontend::ast::Expr; use ruchy::{Parser as RuchyParser, Transpiler}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; /// VM execution mode (OPT-004) #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] @@ -114,10 +114,13 @@ pub fn transpile_for_execution(ast: &Expr, file: &Path) -> Result { } /// Prepare compilation artifacts (complexity: 4) +/// +/// TMPLEAK-1: the binary path is a [`tempfile::TempPath`], so the compiled +/// binary is removed when it drops, on every return path of the caller. pub fn prepare_compilation( rust_code: &str, verbose: bool, -) -> Result<(tempfile::NamedTempFile, PathBuf)> { +) -> Result<(tempfile::NamedTempFile, tempfile::TempPath)> { let temp_source = tempfile::NamedTempFile::new().with_context(|| "Failed to create temporary file")?; fs::write(temp_source.path(), rust_code).with_context(|| "Failed to write temporary file")?; @@ -135,6 +138,8 @@ pub fn prepare_compilation( std::process::id(), unique_id )); + let binary_path = tempfile::TempPath::try_from_path(binary_path) + .with_context(|| "Failed to register temporary binary path")?; Ok((temp_source, binary_path)) } diff --git a/tests/g2b_runfix_3.rs b/tests/g2b_runfix_3.rs index 0a9039217..c1451c30a 100644 --- a/tests/g2b_runfix_3.rs +++ b/tests/g2b_runfix_3.rs @@ -233,3 +233,76 @@ fn test_floatdisp_1_run_matches_compiled_binary() { assert_eq!(compiled, "3 2.5\n3.0\n", "compiled binary"); assert_eq!(ruchy_run(dir.path(), BRIEF_CASE), compiled, "ruchy run"); } + +// --------------------------------------------------------------- TMPLEAK-1 + +/// The compiled program replaces its own executable with a text file, so the +/// next execution fails with "Permission denied" and the handler returns `?`. +const SELF_REPLACING: &str = r#" +fun main() { + let exe = std::env::current_exe().unwrap() + std::fs::remove_file(&exe).unwrap() + std::fs::write(&exe, "not a binary").unwrap() +} +"#; + +const PANICKING: &str = r#" +fun main() { + panic("boom") +} +"#; + +fn leaked_binaries(tmp: &Path) -> Vec { + std::fs::read_dir(tmp) + .expect("read temp dir") + .map(|e| { + e.expect("dir entry") + .file_name() + .to_string_lossy() + .into_owned() + }) + .filter(|name| name.starts_with("ruchy_temp_bin_")) + .collect() +} + +/// Runs `ruchy prog.ruchy` with TMPDIR set to a fresh directory, +/// asserts the command fails, and returns the leaked temp binaries. +fn leaks_after_failing(args: &[&str], src: &str) -> Vec { + let dir = tempfile::tempdir().expect("tempdir"); + let tmp = dir.path().join("tmp"); + std::fs::create_dir(&tmp).expect("create TMPDIR"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.args(&args[..1]) + .arg(&file) + .args(&args[1..]) + .env("TMPDIR", &tmp) + .timeout(Duration::from_secs(180)); + let out = cmd.output().expect("spawn ruchy"); + assert!(!out.status.success(), "the {} run must fail", args[0]); + leaked_binaries(&tmp) +} + +#[test] +fn test_tmpleak_1_property_tests_error_path_removes_binary() { + let leaked = leaks_after_failing(&["property-tests", "--cases", "3"], SELF_REPLACING); + assert!(leaked.is_empty(), "leaked temp binaries: {leaked:?}"); +} + +#[test] +fn test_tmpleak_1_fuzz_error_path_removes_binary() { + let leaked = leaks_after_failing(&["fuzz", "--iterations", "3"], SELF_REPLACING); + assert!(leaked.is_empty(), "leaked temp binaries: {leaked:?}"); +} + +#[test] +fn test_tmpleak_1_property_tests_panicking_program_removes_binary() { + let leaked = leaks_after_failing(&["property-tests", "--cases", "3"], PANICKING); + assert!(leaked.is_empty(), "leaked temp binaries: {leaked:?}"); +} + +#[test] +fn test_tmpleak_1_notebook_failing_program_removes_binary() { + let leaked = leaks_after_failing(&["notebook"], PANICKING); + assert!(leaked.is_empty(), "leaked temp binaries: {leaked:?}"); +} From a8d59620098953e81ee13489f995ca72dfa6cdce Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:23:57 +0200 Subject: [PATCH 086/106] RHLGA-1: file 4 G2b final-round findings (IDXEVAL1, FMTTEMPLATE, FMTEXTRA, PRINTSTRSCOPE) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 3d8c6468e..69805d120 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4600,3 +4600,79 @@ roadmap: labels: - kind:code - cli +- id: IDXEVAL1-1 + github_issue: null + item_type: task + title: 'IDXEVAL1-1: interpreter evaluates the index expression of a compound or nested-index assignment twice (read + write_back); compiled code evaluates it once' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T13:00:00Z + updated: 2026-09-24T13:00:00Z + spec: null + acceptance_criteria: + - 'Repro: v[next()] += 1 or m[f()][j] = x with a side-effecting index' + - 'Acceptance: the index expression is evaluated exactly once in the interpreter; run and compiled output agree' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime +- id: FMTTEMPLATE-1 + github_issue: null + item_type: task + title: 'FMTTEMPLATE-1: interpreter println/print with a non-literal first argument and 2+ args treats it as a format template; compiled code treats only a string literal as one' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T13:00:00Z + updated: 2026-09-24T13:00:00Z + spec: null + acceptance_criteria: + - 'Repro: let s = "{}"; println(s, 1)' + - 'Acceptance: run and the compiled binary print identical text' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime +- id: FMTEXTRA-1 + github_issue: null + item_type: task + title: 'FMTEXTRA-1: interpreter format engine silently ignores extra arguments; rustc rejects them, CHANGELOG says a count mismatch is an error' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T13:00:00Z + updated: 2026-09-24T13:00:00Z + spec: null + acceptance_criteria: + - 'Repro: println("{}", 1, 2)' + - 'Acceptance: the interpreter reports an argument-count error for extra arguments, as CHANGELOG FMTSPEC-1 states' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime +- id: PRINTSTRSCOPE-1 + github_issue: null + item_type: task + title: 'PRINTSTRSCOPE-1: PRINTSTR-1 string tracking is scope-unaware and STRING_RESULT_METHODS matches any receiver, so a shadowing for/closure/match binding or v.repeat(n) on a Vec emits {} and fails rustc' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T13:00:00Z + updated: 2026-09-24T13:00:00Z + spec: null + acceptance_criteria: + - 'Repro: let x = "a"; for x in [[1]] { println(x) } and let v = [1]; println(v.repeat(2))' + - 'Acceptance: shadowed or non-string receivers print with {:?} and compile; string cases keep {}' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler From b150302f0b0c672c078c202471ec25f747787671 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:34:07 +0200 Subject: [PATCH 087/106] RHLGA-1: PRINTSTRSCOPE-1 shadowing binders and non-string receivers print with {:?} - for-loop variables, closure parameters and match/if-let/while-let pattern bindings drop the outer PRINTSTR-1 string record of the name they re-bind while their scope is transpiled; the prior variable_types entries are restored after (with_string_scope). A String-annotated closure parameter is recorded as a string. - replace/repeat count as string results only on a string receiver; to_string and the case/trim methods print as text on every receiver they exist on and are unchanged. - tests/g2b_transfix_4.rs: 7 end-to-end tests (6 RED before the fix). Two compare compiled output only: ruchy run leaks a for-loop variable into the enclosing scope and has no array repeat (interpreter defects). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 6 + docs/roadmaps/roadmap.yaml | 4 +- src/backend/transpiler/control_flow.rs | 11 +- src/backend/transpiler/lambda_transpiler.rs | 6 +- src/backend/transpiler/patterns.rs | 7 +- src/backend/transpiler/print_helpers.rs | 77 +++++++- .../transpiler/string_body_conversion.rs | 7 +- tests/g2b_transfix_4.rs | 170 ++++++++++++++++++ 8 files changed, 271 insertions(+), 17 deletions(-) create mode 100644 tests/g2b_transfix_4.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f03cb19..fb8c07e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,12 @@ Behaviour changes a program can observe are marked **(behaviour)**. - `property-tests`, `fuzz` and `notebook` remove the compiled `ruchy_temp_bin_*` binary on every return path, not only on success (TMPLEAK-1). +- A single-argument `println` prints with `{}` only for a name that is a + string in the current scope: a for-loop variable, closure parameter, or + match / if-let / while-let binding that re-binds a string variable drops + its string record until the scope ends, and `replace` / `repeat` count as + string results only on a string receiver (`[1].repeat(2)` is a `Vec`). + Before, both printed a `Vec` with `{}` and failed rustc (PRINTSTRSCOPE-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 69805d120..3acf7461a 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4661,11 +4661,11 @@ roadmap: github_issue: null item_type: task title: 'PRINTSTRSCOPE-1: PRINTSTR-1 string tracking is scope-unaware and STRING_RESULT_METHODS matches any receiver, so a shadowing for/closure/match binding or v.repeat(n) on a Vec emits {} and fails rustc' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-24T13:00:00Z - updated: 2026-09-24T13:00:00Z + updated: 2026-09-24T16:00:00Z spec: null acceptance_criteria: - 'Repro: let x = "a"; for x in [[1]] { println(x) } and let v = [1]; println(v.repeat(2))' diff --git a/src/backend/transpiler/control_flow.rs b/src/backend/transpiler/control_flow.rs index fa81fe920..75778824a 100644 --- a/src/backend/transpiler/control_flow.rs +++ b/src/backend/transpiler/control_flow.rs @@ -70,8 +70,13 @@ impl Transpiler { // DEFECT-018 FIX: Set loop context flag to enable auto-cloning in function calls let was_in_loop = self.in_loop_context.get(); self.in_loop_context.set(true); - let body_tokens = self.transpile_expr(body)?; + // PRINTSTRSCOPE-1: the loop variable shadows any outer string record. + let loop_pattern = pattern + .cloned() + .unwrap_or_else(|| Pattern::Identifier(var.to_string())); + let body_tokens = self.with_pattern_scope(&loop_pattern, || self.transpile_expr(body)); self.in_loop_context.set(was_in_loop); + let body_tokens = body_tokens?; // If we have a pattern, use it for destructuring if let Some(pat) = pattern { @@ -121,7 +126,7 @@ impl Transpiler { ) -> Result { let expr_tokens = self.transpile_expr(expr)?; let pattern_tokens = self.transpile_pattern(pattern)?; - let then_tokens = self.transpile_expr(then_branch)?; + let then_tokens = self.with_pattern_scope(pattern, || self.transpile_expr(then_branch))?; if let Some(else_expr) = else_branch { let else_tokens = self.transpile_expr(else_expr)?; Ok(quote! { @@ -150,7 +155,7 @@ impl Transpiler { ) -> Result { let expr_tokens = self.transpile_expr(expr)?; let pattern_tokens = self.transpile_pattern(pattern)?; - let body_tokens = self.transpile_expr(body)?; + let body_tokens = self.with_pattern_scope(pattern, || self.transpile_expr(body))?; Ok(quote! { while let #pattern_tokens = #expr_tokens { #body_tokens diff --git a/src/backend/transpiler/lambda_transpiler.rs b/src/backend/transpiler/lambda_transpiler.rs index 796df146f..8764d1cbf 100644 --- a/src/backend/transpiler/lambda_transpiler.rs +++ b/src/backend/transpiler/lambda_transpiler.rs @@ -19,7 +19,8 @@ impl Transpiler { params: &[Param], body: &Expr, ) -> Result { - let body_tokens = self.transpile_expr(body)?; + // PRINTSTRSCOPE-1: parameters shadow outer string records. + let body_tokens = self.with_param_scope(params, || self.transpile_expr(body))?; if params.is_empty() { return Ok(quote! { move || #body_tokens }); @@ -57,7 +58,8 @@ impl Transpiler { params: &[Param], body: &Expr, ) -> Result { - let body_tokens = self.transpile_expr(body)?; + // PRINTSTRSCOPE-1: parameters shadow outer string records. + let body_tokens = self.with_param_scope(params, || self.transpile_expr(body))?; if params.is_empty() { return Ok(quote! { move || async move { #body_tokens } }); diff --git a/src/backend/transpiler/patterns.rs b/src/backend/transpiler/patterns.rs index 27f2b599c..47c41d949 100644 --- a/src/backend/transpiler/patterns.rs +++ b/src/backend/transpiler/patterns.rs @@ -29,10 +29,13 @@ impl Transpiler { let mut arm_tokens = Vec::new(); for arm in arms { let pattern_tokens = self.transpile_pattern(&arm.pattern)?; - let body_tokens = self.transpile_expr(&arm.body)?; + // PRINTSTRSCOPE-1: arm bindings shadow outer string records. + let body_tokens = + self.with_pattern_scope(&arm.pattern, || self.transpile_expr(&arm.body))?; // Handle pattern guards if present if let Some(guard_expr) = &arm.guard { - let guard_tokens = self.transpile_expr(guard_expr)?; + let guard_tokens = + self.with_pattern_scope(&arm.pattern, || self.transpile_expr(guard_expr))?; arm_tokens.push(quote! { #pattern_tokens if #guard_tokens => #body_tokens }); diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index 97ecbd353..369b9654b 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -8,8 +8,9 @@ //! //! **EXTREME TDD Round 64**: Extracted from statements.rs and mod.rs for modularization. +use super::pattern_bindings::extract_pattern_bindings; use super::Transpiler; -use crate::frontend::ast::{Expr, ExprKind, Literal, Type, TypeKind}; +use crate::frontend::ast::{Expr, ExprKind, Literal, Param, Pattern, Type, TypeKind}; use anyhow::Result; use proc_macro2::TokenStream; use quote::quote; @@ -245,7 +246,8 @@ impl Transpiler { /// PRINTSTR-1: the type tag `variable_types` holds for a string binding. const STRING_VAR_TYPE: &str = "String"; -/// PRINTSTR-1: methods whose result is a `String`/`&str` in the transpiled Rust. +/// PRINTSTR-1: methods whose result prints as text on every receiver they +/// exist on (`to_string` on anything, the case/trim methods on `str`/`char`). const STRING_RESULT_METHODS: &[&str] = &[ "to_string", "to_uppercase", @@ -253,10 +255,12 @@ const STRING_RESULT_METHODS: &[&str] = &[ "trim", "trim_start", "trim_end", - "replace", - "repeat", ]; +/// PRINTSTRSCOPE-1: methods whose result is a `String` only on a string +/// receiver (`[1].repeat(2)` is a `Vec`, `Option::replace` an `Option`). +const STRING_RECEIVER_METHODS: &[&str] = &["replace", "repeat"]; + impl Transpiler { /// PRINTSTR-1: `{}` for an expression known to be a string, `{:?}` for /// anything else (a number prints the same either way). (complexity: 2) @@ -278,7 +282,9 @@ impl Transpiler { name.trim_end_matches('!') == "format" } ExprKind::Call { func, args } => self.is_format_fn_call(func, args), - ExprKind::MethodCall { method, .. } => STRING_RESULT_METHODS.contains(&method.as_str()), + ExprKind::MethodCall { + receiver, method, .. + } => self.is_string_method_result(receiver, method), ExprKind::Identifier(name) => self .variable_types .borrow() @@ -300,7 +306,7 @@ impl Transpiler { /// PRINTSTR-1: parameters of the function being transpiled; string /// records of earlier functions are dropped first. (complexity: 2) - pub(crate) fn track_string_params(&self, params: &[crate::frontend::ast::Param]) { + pub(crate) fn track_string_params(&self, params: &[Param]) { self.variable_types .borrow_mut() .retain(|_, t| t != STRING_VAR_TYPE); @@ -309,6 +315,65 @@ impl Transpiler { } } + /// PRINTSTRSCOPE-1: `receiver.method(..)` is a string: a text-printing + /// method, or `replace`/`repeat` on a string receiver. (complexity: 3) + fn is_string_method_result(&self, receiver: &Expr, method: &str) -> bool { + STRING_RESULT_METHODS.contains(&method) + || (STRING_RECEIVER_METHODS.contains(&method) && self.is_display_string(receiver)) + } + + /// PRINTSTRSCOPE-1: run `f` while each `(name, is_string)` binder + /// re-binds `name` (a shadowing binder drops the outer string record); + /// the prior `variable_types` entries of those names are restored after. + /// (complexity: 3) + pub(crate) fn with_string_scope( + &self, + binders: &[(String, bool)], + f: impl FnOnce() -> T, + ) -> T { + let saved: Vec<(String, Option)> = binders + .iter() + .map(|(name, _)| { + ( + name.clone(), + self.variable_types.borrow().get(name).cloned(), + ) + }) + .collect(); + for (name, is_string) in binders { + self.set_string_var(name, *is_string); + } + let result = f(); + let mut types = self.variable_types.borrow_mut(); + for (name, entry) in saved.into_iter().rev() { + match entry { + Some(ty) => types.insert(name, ty), + None => types.remove(&name), + }; + } + result + } + + /// PRINTSTRSCOPE-1: run `f` in the scope of the names `pattern` binds + /// (for-loop, match arm, if-let, while-let). (complexity: 1) + pub(crate) fn with_pattern_scope(&self, pattern: &Pattern, f: impl FnOnce() -> T) -> T { + let binders: Vec<(String, bool)> = extract_pattern_bindings(pattern) + .into_iter() + .map(|name| (name, false)) + .collect(); + self.with_string_scope(&binders, f) + } + + /// PRINTSTRSCOPE-1: run `f` in the scope of closure parameters; a + /// `String`/`&str`-annotated parameter is a string. (complexity: 1) + pub(crate) fn with_param_scope(&self, params: &[Param], f: impl FnOnce() -> T) -> T { + let binders: Vec<(String, bool)> = params + .iter() + .map(|p| (p.name(), is_string_annotation(&p.ty))) + .collect(); + self.with_string_scope(&binders, f) + } + /// PRINTSTR-1: set or clear the string record of `name`. (complexity: 3) fn set_string_var(&self, name: &str, is_string: bool) { let mut types = self.variable_types.borrow_mut(); diff --git a/src/backend/transpiler/string_body_conversion.rs b/src/backend/transpiler/string_body_conversion.rs index e320be8f1..feb20f75d 100644 --- a/src/backend/transpiler/string_body_conversion.rs +++ b/src/backend/transpiler/string_body_conversion.rs @@ -198,10 +198,13 @@ impl Transpiler { for arm in arms { let pattern_tokens = self.transpile_pattern(&arm.pattern)?; - let body_tokens = self.convert_arm_body_to_string(&arm.body)?; + // PRINTSTRSCOPE-1: arm bindings shadow outer string records. + let body_tokens = self + .with_pattern_scope(&arm.pattern, || self.convert_arm_body_to_string(&arm.body))?; if let Some(guard_expr) = &arm.guard { - let guard_tokens = self.transpile_expr(guard_expr)?; + let guard_tokens = + self.with_pattern_scope(&arm.pattern, || self.transpile_expr(guard_expr))?; arm_tokens.push(quote! { #pattern_tokens if #guard_tokens => #body_tokens }); diff --git a/tests/g2b_transfix_4.rs b/tests/g2b_transfix_4.rs new file mode 100644 index 000000000..8638f009e --- /dev/null +++ b/tests/g2b_transfix_4.rs @@ -0,0 +1,170 @@ +//! RHLGA-1 transpiler fixes (fourth batch), each checked end to end: +//! `ruchy transpile`, `ruchy compile` (rustc), run the binary, compare exact +//! stdout, and require `ruchy run` to print the same. +//! +//! - PRINTSTRSCOPE-1: the PRINTSTR-1 string record of a name is dropped +//! while a for-loop variable, closure parameter, or match / if-let / +//! while-let pattern re-binds it (and restored after), and `replace` / +//! `repeat` count as string results only on a string receiver. Before, a +//! shadowing binding or `v.repeat(n)` on a `Vec` printed with `{}` (E0277). + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +/// (stdout, stderr) of a command that must succeed. +fn output_of(mut cmd: Command, what: &str) -> (String, String) { + let out = cmd.output().expect("spawn command"); + let stdout = String::from_utf8(out.stdout).expect("utf8 stdout"); + let stderr = String::from_utf8(out.stderr).expect("utf8 stderr"); + assert!(out.status.success(), "{what} failed:\n{stdout}{stderr}"); + (stdout, stderr) +} + +fn transpile(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + output_of(cmd, &format!("ruchy transpile of:\n{src}\n")).0 +} + +/// `ruchy compile` the source, run the binary, return (stdout, stderr). +fn compile_and_run(src: &str) -> (String, String) { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let bin = dir.path().join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(90)); + output_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + output_of(run, "compiled binary") +} + +fn interpret(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + output_of(cmd, &format!("ruchy run of:\n{src}\n")).0 +} + +fn main_with(body: &str) -> String { + format!("fun main() {{\n{body}\n}}\n") +} + +/// Compiled stdout is `expected`; with `interp`, `ruchy run` agrees. +fn check(src: &str, expected: &str, interp: bool) { + let (stdout, _) = compile_and_run(src); + assert_eq!( + stdout, + expected, + "compiled stdout of:\n{src}\n{}", + transpile(src) + ); + if interp { + assert_eq!(interpret(src), expected, "interpreter stdout of:\n{src}"); + } +} + +// ----------------------------------------------------------- PRINTSTRSCOPE-1 + +#[test] +fn test_printstrscope_1_for_variable_shadows_string() { + let src = main_with( + r#" let x = "a" + for x in [[1]] { + println(x) + } + println(x)"#, + ); + // `ruchy run` leaks the loop variable into the enclosing scope (prints + // `[1]` twice): an interpreter defect outside this transpiler fix. + check(&src, "[1]\na\n", false); +} + +#[test] +fn test_printstrscope_1_closure_param_shadows_string() { + let src = main_with( + r#" let x = "a" + let f = |x| println(x) + f([5]) + println(x)"#, + ); + check(&src, "[5]\na\n", true); +} + +#[test] +fn test_printstrscope_1_closure_string_param_prints_text() { + let src = main_with( + r#" let f = |s: String| println(s) + f("hey".to_string()) + println("end")"#, + ); + check(&src, "hey\nend\n", true); +} + +#[test] +fn test_printstrscope_1_match_arm_binding_shadows_string() { + let src = main_with( + r#" let x = "a" + let o = Some([1, 2]) + match o { + Some(x) => println(x), + None => println("none"), + } + println(x)"#, + ); + check(&src, "[1, 2]\na\n", true); +} + +#[test] +fn test_printstrscope_1_if_let_binding_shadows_string() { + let src = main_with( + r#" let x = "a" + if let Some(x) = Some([3]) { + println(x) + } + println(x)"#, + ); + check(&src, "[3]\na\n", true); +} + +#[test] +fn test_printstrscope_1_repeat_on_vec_is_not_a_string() { + let src = main_with( + r#" let v = [1] + println(v.repeat(2))"#, + ); + // `ruchy run` has no array `repeat` ("Unknown array method: repeat"): + // an interpreter gap outside this transpiler fix. + check(&src, "[1, 1]\n", false); +} + +#[test] +fn test_printstrscope_1_string_receiver_methods_keep_text() { + let src = main_with( + r#" let s = "ab" + println(s.repeat(2)) + println("a-b".replace("-", "+")) + println(s.trim().replace("a", "z").repeat(2))"#, + ); + check(&src, "abab\na+b\nzbzb\n", true); +} From 80d4b85edfa5af31cb449ff62b5a35c50202df67 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:34:03 +0200 Subject: [PATCH 088/106] RHLGA-1: IDXEVAL1-1 each index of an assigned place is evaluated once Compound assignment, nested-index assignment, field assignment through an indexed chain and in-place array methods on an element freeze the place first: every index expression is evaluated once and replaced by its value's literal, so the read and the write-back run no index expression again. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 + docs/roadmaps/roadmap.yaml | 4 +- src/runtime/interpreter_control_flow.rs | 38 +++++ src/runtime/interpreter_methods_dispatch.rs | 1 + tests/g2b_runfix_4.rs | 162 ++++++++++++++++++++ 5 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 tests/g2b_runfix_4.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fb8c07e46..ac3eeb0e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. its string record until the scope ends, and `replace` / `repeat` count as string results only on a string receiver (`[1].repeat(2)` is a `Vec`). Before, both printed a `Vec` with `{}` and failed rustc (PRINTSTRSCOPE-1). +- The interpreter evaluates each index expression of a compound assignment + (`v[next()] += 1`), a nested-index assignment (`m[f()][1] = 9`) or an + in-place array method on an element (`m[f()].push(x)`) once, as compiled + code does; it used to run it again to write the place back (IDXEVAL1-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 3acf7461a..8014bb5f0 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4604,11 +4604,11 @@ roadmap: github_issue: null item_type: task title: 'IDXEVAL1-1: interpreter evaluates the index expression of a compound or nested-index assignment twice (read + write_back); compiled code evaluates it once' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-24T13:00:00Z - updated: 2026-09-24T13:00:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: v[next()] += 1 or m[f()][j] = x with a side-effecting index' diff --git a/src/runtime/interpreter_control_flow.rs b/src/runtime/interpreter_control_flow.rs index 21a45bdc2..71fee237f 100644 --- a/src/runtime/interpreter_control_flow.rs +++ b/src/runtime/interpreter_control_flow.rs @@ -445,6 +445,7 @@ impl Interpreter { field: &str, val: Value, ) -> Result { + let object = &self.freeze_place(object)?; let parent = match &object.kind { ExprKind::Identifier(obj_name) => self.lookup_variable(obj_name)?, ExprKind::FieldAccess { .. } | ExprKind::IndexAccess { .. } => { @@ -508,6 +509,7 @@ impl Interpreter { index: &Expr, val: Value, ) -> Result { + let object = &self.freeze_place(object)?; let container = self.eval_expr(object)?; let key = self.eval_expr(index)?; let updated = with_element(container, &key, val.clone())?; @@ -534,12 +536,48 @@ impl Interpreter { "Invalid compound assignment target".to_string(), )); } + let target = &self.freeze_place(target)?; let current = self.eval_expr(target)?; let rhs = self.eval_expr(value)?; let new_val = self.apply_binary_op(¤t, op, &rhs)?; self.write_back(target, new_val.clone())?; Ok(new_val) } + + /// IDXEVAL1-1: `place` with every index expression of its chain replaced + /// by the literal of its value, evaluated once, left to right. Reading the + /// frozen place and writing it back then runs no index expression again, + /// so `v[next()] += 1` and `m[f()][j] = x` call `next`/`f` once, as + /// compiled code does. An index value with no literal form (a range, a + /// float) is left as written; the write rejects such a key anyway. + /// Complexity: 3 + pub(crate) fn freeze_place(&mut self, place: &Expr) -> Result { + let mut frozen = place.clone(); + match &mut frozen.kind { + ExprKind::FieldAccess { object, .. } => **object = self.freeze_place(object)?, + ExprKind::IndexAccess { object, index } => { + **object = self.freeze_place(object)?; + let key = self.eval_expr(index)?; + if let Some(literal) = key_literal(&key) { + index.kind = ExprKind::Literal(literal); + } + } + _ => {} + } + Ok(frozen) + } +} + +/// IDXEVAL1-1: the literal that evaluates to the index `key`, for the key +/// types an element write accepts (an integer, a string, an atom). +/// Complexity: 4 +fn key_literal(key: &Value) -> Option { + match key { + Value::Integer(i) => Some(Literal::Integer(*i, None)), + Value::String(s) => Some(Literal::String(s.to_string())), + Value::Atom(a) => Some(Literal::Atom(a.clone())), + _ => None, + } } /// DICTIDX-1: `container` with the element at `key` replaced by `val`: an diff --git a/src/runtime/interpreter_methods_dispatch.rs b/src/runtime/interpreter_methods_dispatch.rs index e35a977af..f748336c9 100644 --- a/src/runtime/interpreter_methods_dispatch.rs +++ b/src/runtime/interpreter_methods_dispatch.rs @@ -110,6 +110,7 @@ impl Interpreter { if !is_chain || !is_place_expr(receiver) || !is_in_place_array_method(method, args.len()) { return Ok(None); } + let receiver = &self.freeze_place(receiver)?; let Value::Array(arr) = self.eval_expr(receiver)? else { return Ok(None); }; diff --git a/tests/g2b_runfix_4.rs b/tests/g2b_runfix_4.rs new file mode 100644 index 000000000..193bd5a2f --- /dev/null +++ b/tests/g2b_runfix_4.rs @@ -0,0 +1,162 @@ +//! RHLGA-1 G2b run fixes, part 4. +//! +//! - IDXEVAL1-1: `ruchy run` evaluated the index expression of a compound +//! assignment (`v[next()] += 1`) or a nested-index assignment +//! (`m[f()][1] = 9`) twice: once to read the place and once to write it +//! back. Compiled code evaluates it once. +//! - FMTTEMPLATE-1: `ruchy run` treated a non-literal first argument of +//! `println(s, 1)` as a format template; the transpiler treats only a +//! string literal as one and prints every argument separated by spaces. +//! - FMTEXTRA-1: the format engine ignored an extra positional argument +//! (`println("{}", 1, 2)`); rustc rejects an argument the template never +//! uses, so `ruchy run` now reports an error. +//! +//! Every accepted case compares `ruchy run` with the binary `ruchy compile` +//! builds from the same source. + +use assert_cmd::Command; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +fn write_source(dir: &Path, src: &str) -> PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn ruchy() -> Command { + Command::cargo_bin("ruchy").expect("ruchy binary is built for tests") +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn run_cmd(dir: &Path, src: &str) -> Command { + let mut cmd = ruchy(); + cmd.arg("run") + .arg(write_source(dir, src)) + .timeout(Duration::from_secs(10)); + cmd +} + +fn compile_cmd(dir: &Path, src: &str) -> (Command, PathBuf) { + let bin = dir.join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(write_source(dir, src)) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(180)); + (cmd, bin) +} + +fn compiled_run(dir: &Path, src: &str) -> String { + let (cmd, bin) = compile_cmd(dir, src); + stdout_of(cmd, "ruchy compile"); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(10)); + stdout_of(run, "compiled binary") +} + +/// The compiled binary prints `expected`, and `ruchy run` prints it too. +fn assert_run_matches_compiled(src: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!(compiled_run(dir.path(), src), expected, "compiled binary"); + assert_eq!( + stdout_of(run_cmd(dir.path(), src), "ruchy run"), + expected, + "ruchy run" + ); +} + +/// `ruchy run` prints `expected`: the output Rust semantics give. Used where +/// the transpiler cannot build the program yet. +fn assert_run_prints(src: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!( + stdout_of(run_cmd(dir.path(), src), "ruchy run"), + expected, + "ruchy run" + ); +} + +// ------------------------------------------------------------- IDXEVAL1-1 + +const COUNTER: &str = r#" +let mut calls = 0 +fun next() -> i64 { + calls += 1 + println("index {}", calls) + 0 +} +"#; + +fn with_counter(main: &str) -> String { + format!("{COUNTER}{main}") +} + +#[test] +fn test_idxeval1_1_compound_index_evaluates_index_once() { + let src = with_counter( + r#" +fun main() { + let mut v = [10, 20] + v[next()] += 1 + v[next()] *= 3 + println("{:?} {}", v, calls) +} +"#, + ); + assert_run_matches_compiled(&src, "index 1\nindex 2\n[33, 20] 2\n"); +} + +#[test] +fn test_idxeval1_1_nested_index_assign_evaluates_index_once() { + let src = with_counter( + r#" +fun main() { + let mut m = [[1, 2], [3, 4]] + m[next()][1] = 9 + println("{:?} {}", m, calls) +} +"#, + ); + assert_run_matches_compiled(&src, "index 1\n[[1, 9], [3, 4]] 1\n"); +} + +#[test] +fn test_idxeval1_1_nested_compound_evaluates_each_index_once() { + let src = with_counter( + r#" +fun main() { + let mut m = [[1, 2], [3, 4]] + m[next()][next()] += 5 + println("{:?} {}", m, calls) +} +"#, + ); + assert_run_matches_compiled(&src, "index 1\nindex 2\n[[6, 2], [3, 4]] 2\n"); +} + +/// The transpiler emits a nested array literal as a fixed-size array, which +/// has no `push`, so this case is checked against `ruchy run` alone. +#[test] +fn test_idxeval1_1_in_place_push_on_indexed_element_evaluates_index_once() { + let src = with_counter( + r#" +fun main() { + let mut m = [[1], [2]] + m[next()].push(7) + println("{:?} {}", m, calls) +} +"#, + ); + assert_run_prints(&src, "index 1\n[[1, 7], [2]] 1\n"); +} From 4f84a40f50b6c46d6a4ae1709720f17fc7b5ab85 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:36:01 +0200 Subject: [PATCH 089/106] RHLGA-1: FMTTEMPLATE-1 only a string literal is a format template The interpreter used a non-literal first argument with two or more arguments as the template (println(s, 1) printed 1); the compiled binary joins every argument with spaces (prints {} 1). Only a string-literal first argument is a template now. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 ++++ docs/roadmaps/roadmap.yaml | 4 ++-- src/runtime/interpreter_format_calls.rs | 5 ++++- tests/g2b_runfix_4.rs | 19 +++++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac3eeb0e1..ba5edb844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,6 +147,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. (`v[next()] += 1`), a nested-index assignment (`m[f()][1] = 9`) or an in-place array method on an element (`m[f()].push(x)`) once, as compiled code does; it used to run it again to write the place back (IDXEVAL1-1). +- **(behaviour)** The interpreter treats the first argument of `println`, + `print`, `eprintln`, `eprint` and `format` as a format template only when + it is a string literal, as the transpiler does: `let s = "{}"; println(s, 1)` + prints `{} 1`, not `1` (FMTTEMPLATE-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 8014bb5f0..f6c48615b 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4623,11 +4623,11 @@ roadmap: github_issue: null item_type: task title: 'FMTTEMPLATE-1: interpreter println/print with a non-literal first argument and 2+ args treats it as a format template; compiled code treats only a string literal as one' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-24T13:00:00Z - updated: 2026-09-24T13:00:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let s = "{}"; println(s, 1)' diff --git a/src/runtime/interpreter_format_calls.rs b/src/runtime/interpreter_format_calls.rs index b4cd84789..b5a8f2276 100644 --- a/src/runtime/interpreter_format_calls.rs +++ b/src/runtime/interpreter_format_calls.rs @@ -135,7 +135,10 @@ impl Interpreter { args: &[Expr], ) -> Result { let (positional, named) = self.eval_format_args(args)?; - let template = args.first().is_some_and(is_string_literal) || positional.len() > 1; + // FMTTEMPLATE-1: only a string literal is a format template, as in + // the transpiler; `println(s, 1)` prints its arguments joined by + // spaces. + let template = args.first().is_some_and(is_string_literal); let resolve = |name: &str| { named .iter() diff --git a/tests/g2b_runfix_4.rs b/tests/g2b_runfix_4.rs index 193bd5a2f..5106a4ce7 100644 --- a/tests/g2b_runfix_4.rs +++ b/tests/g2b_runfix_4.rs @@ -160,3 +160,22 @@ fun main() { ); assert_run_prints(&src, "index 1\n[[1, 7], [2]] 1\n"); } + +// ---------------------------------------------------------- FMTTEMPLATE-1 + +#[test] +fn test_fmttemplate_1_variable_first_arg_is_not_a_template() { + assert_run_matches_compiled( + r#" +fun main() { + let s = "{}" + println(s, 1) + let t = "x={} y={}" + println(t, 2, 3) + println("a", 1) + println("{} and {}", 4, 5) +} +"#, + "{} 1\nx={} y={} 2 3\na 1\n4 and 5\n", + ); +} From 3e54da2f4126773244eb51688553bf1f93b416f9 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:38:39 +0200 Subject: [PATCH 090/106] RHLGA-1: FMTEXTRA-1 an unused positional format argument is an error render records which positional argument each {} or {N} field uses and reports the first one no field references, following rustc's rule; it used to ignore extra arguments. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 +++ docs/roadmaps/roadmap.yaml | 4 +-- src/runtime/fmt_spec.rs | 31 +++++++++++++++++++-- src/runtime/fmt_spec_tests.rs | 45 +++++++++++++++++++++++++++++++ tests/g2b_runfix_4.rs | 51 +++++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba5edb844..d04a59f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,6 +151,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. `print`, `eprintln`, `eprint` and `format` as a format template only when it is a string literal, as the transpiler does: `let s = "{}"; println(s, 1)` prints `{} 1`, not `1` (FMTTEMPLATE-1). +- **(behaviour)** A format string that never uses one of its positional + arguments is an error in the interpreter, as rustc rejects it: + `println("{}", 1, 2)` and `println("{0} {0}", 1, 2)` fail with + "argument never used"; an explicit `{1}` counts as a use (FMTEXTRA-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index f6c48615b..5473bc6a9 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4642,11 +4642,11 @@ roadmap: github_issue: null item_type: task title: 'FMTEXTRA-1: interpreter format engine silently ignores extra arguments; rustc rejects them, CHANGELOG says a count mismatch is an error' - status: planned + status: completed priority: medium assigned_to: null created: 2026-09-24T13:00:00Z - updated: 2026-09-24T13:00:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: println("{}", 1, 2)' diff --git a/src/runtime/fmt_spec.rs b/src/runtime/fmt_spec.rs index b902d1e0a..a9b2deac4 100644 --- a/src/runtime/fmt_spec.rs +++ b/src/runtime/fmt_spec.rs @@ -272,18 +272,44 @@ pub fn render( ) -> Result { let mut out = String::new(); let mut next = 0; + let mut used = vec![false; args.len()]; for piece in pieces { match piece { Piece::Text(text) => out.push_str(text), Piece::Field(arg, spec) => { + mark_used(arg, next, &mut used); let value = resolve(arg, args, &mut next, named)?; out.push_str(&format_value(&value, spec)?); } } } + check_all_used(&used)?; Ok(out) } +/// FMTEXTRA-1: record the positional argument `arg` refers to (`{}` takes +/// the next one, `{0}` names one); a named field uses none. +fn mark_used(arg: &ArgRef, next: usize, used: &mut [bool]) { + let slot = match arg { + ArgRef::Next => next, + ArgRef::Index(i) => *i, + ArgRef::Name(_) => return, + }; + if let Some(flag) = used.get_mut(slot) { + *flag = true; + } +} + +/// FMTEXTRA-1: Rust rejects a positional argument the template never uses. +fn check_all_used(used: &[bool]) -> Result<(), String> { + match used.iter().position(|u| !u) { + Some(i) => Err(format!( + "argument never used: positional argument {i} is not referenced by the format string" + )), + None => Ok(()), + } +} + fn resolve( arg: &ArgRef, args: &[Value], @@ -326,8 +352,9 @@ pub fn format_str( /// /// With `template`, the first value is the format string. A template with no /// fields and extra arguments keeps the historical ruchy form -/// (`println("a", 1)` prints `a 1`), which Rust rejects at compile time. -/// Without a template the values are joined by spaces. +/// (`println("a", 1)` prints `a 1`), as the transpiler does. A template with +/// fields must use every argument (FMTEXTRA-1). Without a template the values +/// are joined by spaces. pub fn format_call( values: &[Value], template: bool, diff --git a/src/runtime/fmt_spec_tests.rs b/src/runtime/fmt_spec_tests.rs index ab0926186..ef6f1f61b 100644 --- a/src/runtime/fmt_spec_tests.rs +++ b/src/runtime/fmt_spec_tests.rs @@ -191,3 +191,48 @@ proptest! { prop_assert_eq!(ours(idx, &s(&v), w, p), rust_ref!(idx, v.as_str(), w, p)); } } + +#[test] +fn test_fmtextra_1_extra_positional_argument_is_an_error() { + let err = fmt("{}", &[Value::Integer(1), Value::Integer(2)]).unwrap_err(); + assert!(err.contains("argument never used"), "{err}"); + assert!(err.contains("argument 1"), "{err}"); +} + +#[test] +fn test_fmtextra_1_unreferenced_explicit_index_is_an_error() { + let err = fmt("{0} {0}", &[Value::Integer(1), Value::Integer(2)]).unwrap_err(); + assert!(err.contains("argument 1"), "{err}"); + let err = fmt("{1}", &[Value::Integer(1), Value::Integer(2)]).unwrap_err(); + assert!(err.contains("argument 0"), "{err}"); +} + +#[test] +fn test_fmtextra_1_explicit_indices_count_as_uses() { + let args = [Value::Integer(1), Value::Integer(2)]; + assert_eq!( + fmt("{1} {0} {}", &args).unwrap(), + format!("{1} {0} {}", 1, 2) + ); + assert_eq!(fmt("{} {0}", &args[..1]).unwrap(), format!("{} {0}", 1)); + assert_eq!(fmt("{0}{}{}", &args).unwrap(), format!("{0}{}{}", 1, 2)); +} + +#[test] +fn test_fmtextra_1_named_fields_leave_positional_arguments_unused() { + let named = |n: &str| (n == "x").then_some(Value::Integer(5)); + let err = format_str("{x}", &[Value::Integer(1)], &named).unwrap_err(); + assert!(err.contains("argument never used"), "{err}"); + assert_eq!( + format_str("{x} {}", &[Value::Integer(1)], &named).unwrap(), + "5 1" + ); +} + +#[test] +fn test_fmtextra_1_format_call_template_with_extra_argument_errors() { + let named = |_: &str| None; + let values = [s("{}"), Value::Integer(1), Value::Integer(2)]; + assert!(format_call(&values, true, &named).is_err()); + assert_eq!(format_call(&values, false, &named).unwrap(), "{} 1 2"); +} diff --git a/tests/g2b_runfix_4.rs b/tests/g2b_runfix_4.rs index 5106a4ce7..477b359cc 100644 --- a/tests/g2b_runfix_4.rs +++ b/tests/g2b_runfix_4.rs @@ -87,6 +87,18 @@ fn assert_run_prints(src: &str, expected: &str) { ); } +/// `ruchy compile` rejects `src`, and `ruchy run` fails with `needle` on +/// stderr. +fn assert_both_reject(src: &str, needle: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + let (mut cmd, _) = compile_cmd(dir.path(), src); + cmd.assert().failure(); + let out = run_cmd(dir.path(), src).output().expect("spawn ruchy run"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(!out.status.success(), "ruchy run accepted: {stderr}"); + assert!(stderr.contains(needle), "stderr lacks {needle:?}: {stderr}"); +} + // ------------------------------------------------------------- IDXEVAL1-1 const COUNTER: &str = r#" @@ -179,3 +191,42 @@ fun main() { "{} 1\nx={} y={} 2 3\na 1\n4 and 5\n", ); } + +// ------------------------------------------------------------- FMTEXTRA-1 + +#[test] +fn test_fmtextra_1_extra_positional_argument_is_rejected() { + assert_both_reject( + r#" +fun main() { + println("{} {}", 1, 2, 3) +} +"#, + "argument never used", + ); +} + +#[test] +fn test_fmtextra_1_unreferenced_explicit_index_is_rejected() { + assert_both_reject( + r#" +fun main() { + println("{0} {0}", 1, 2) +} +"#, + "argument never used", + ); +} + +#[test] +fn test_fmtextra_1_explicit_indices_count_as_uses() { + assert_run_matches_compiled( + r#" +fun main() { + println("{1} {0} {}", 1, 2) + println("{} {0}", 3) +} +"#, + "2 1 1\n3 3\n", + ); +} From 1a13073ce7b4813a16e136394add648d8ac68420 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:43:16 +0200 Subject: [PATCH 091/106] RHLGA-1: file FORLEAK, ARRREPEAT, NESTARRVEC, CLOSUREUNIT, FORMATVAL (found by G2b fix workers) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 95 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 5473bc6a9..320994ae7 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4676,3 +4676,98 @@ roadmap: labels: - kind:code - transpiler +- id: FORLEAK-1 + github_issue: null + item_type: task + title: 'FORLEAK-1: interpreter for-loop variable leaks into the enclosing scope and overwrites a same-named outer binding' + status: planned + priority: critical + assigned_to: null + created: 2026-09-24T14:30:00Z + updated: 2026-09-24T14:30:00Z + spec: null + acceptance_criteria: + - 'Repro: let x = "a"; for x in [[1]] { }; println(x) prints [1]' + - 'Acceptance: after the loop the outer x is unchanged ("a"); run matches the compiled binary' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime +- id: ARRREPEAT-1 + github_issue: null + item_type: task + title: 'ARRREPEAT-1: interpreter arrays have no repeat method (Unknown array method: repeat); Rust slices have repeat' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T14:30:00Z + updated: 2026-09-24T14:30:00Z + spec: null + acceptance_criteria: + - 'Repro: let v = [1]; println("{:?}", v.repeat(2))' + - 'Acceptance: run prints [1, 1] like the compiled binary' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime +- id: NESTARRVEC-1 + github_issue: null + item_type: task + title: 'NESTARRVEC-1: a nested array literal transpiles to a fixed-size array, so m[i].push(x) fails rustc E0599' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T14:30:00Z + updated: 2026-09-24T14:30:00Z + spec: null + acceptance_criteria: + - 'Repro: let mut m = [[1], [2]]; m[0].push(9); println("{:?}", m)' + - 'Acceptance: compiled prints [[1, 9], [2]] like ruchy run' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler +- id: CLOSUREUNIT-1 + github_issue: null + item_type: task + title: 'CLOSUREUNIT-1: a unit-returning closure call as the last statement of main is printed as () (MAINUNIT-1 covers user functions only)' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T14:30:00Z + updated: 2026-09-24T14:30:00Z + spec: null + acceptance_criteria: + - 'Repro: let f = |s| println(s); f("hi") as the last statement of main' + - 'Acceptance: compiled prints only hi' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler +- id: FORMATVAL-1 + github_issue: null + item_type: task + title: 'FORMATVAL-1: the format builtin called as a value (eval_builtin.rs:318) still treats a non-literal first argument as a template when args.len() > 1' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T14:30:00Z + updated: 2026-09-24T14:30:00Z + spec: null + acceptance_criteria: + - 'Repro: let t = "{}"; let g = format; println(g(t, 1))' + - 'Acceptance: run agrees with compiled / FMTTEMPLATE-1 rule' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime From d9e8bc15d67369ed94a1f99b98b6ac1abb248e7b Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:46:07 +0200 Subject: [PATCH 092/106] =?UTF-8?q?RHLGA-1:=20G2b=20intel=20run=20at=20316?= =?UTF-8?q?b7433=20=E2=80=94=205=20new=20failures=20root-caused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - eval_builtin_comprehensive test_print_no_newline pinned print("A") as "A"; PRINTSTR-1/FMTSPEC-1 print the text, as the compiled print! does - opt_022 hidden_class_transition pinned {} of 10.0 as 10.0; FLOATDISP-1 prints 10 - transpiler_deadlock_fix properties generated keyword names (fun); assume the lexer reads the name as one identifier - cli_unify_003 prop_030/031 (10K spawns) ran 113-119s against a 120s kill; the property override now also matches property-test binaries Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- .config/nextest.toml | 5 +++-- tests/eval_builtin_comprehensive.rs | 4 ++-- tests/opt_022_inline_caching.rs | 2 +- tests/transpiler_deadlock_fix.rs | 9 +++++++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 0048196a5..2b4310675 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -49,8 +49,9 @@ filter = 'test(env_set_current_dir)' threads-required = 1 [[profile.default.overrides]] -# Property tests need more time -filter = 'test(/property/)' +# Property tests need more time; a property-test binary's tests count too +# (cli_unify_003_property_tests spawns ruchy 10K times per property) +filter = 'test(/property/) | binary(/property/)' slow-timeout = { period = "90s", terminate-after = 2 } [[profile.coverage.overrides]] diff --git a/tests/eval_builtin_comprehensive.rs b/tests/eval_builtin_comprehensive.rs index a8bfb126c..0f4eeb583 100644 --- a/tests/eval_builtin_comprehensive.rs +++ b/tests/eval_builtin_comprehensive.rs @@ -345,13 +345,13 @@ fn test_println_multiple_args() { #[test] fn test_print_no_newline() { - // print() quotes strings, so output is "A""B" not AB + // print() writes a string's text, as the compiled print! does ruchy_cmd() .arg("-e") .arg("print(\"A\"); print(\"B\")") .assert() .success() - .stdout(predicate::str::contains("\"A\"\"B\"")); + .stdout(predicate::str::contains("AB")); } #[test] diff --git a/tests/opt_022_inline_caching.rs b/tests/opt_022_inline_caching.rs index 566afc95b..eebccb28f 100644 --- a/tests/opt_022_inline_caching.rs +++ b/tests/opt_022_inline_caching.rs @@ -78,7 +78,7 @@ fun main() { .arg(code) .assert() .success() - .stdout("10.0\n"); + .stdout("10\n"); } // ============================================================================ diff --git a/tests/transpiler_deadlock_fix.rs b/tests/transpiler_deadlock_fix.rs index 13037ee51..1d638caf9 100644 --- a/tests/transpiler_deadlock_fix.rs +++ b/tests/transpiler_deadlock_fix.rs @@ -267,6 +267,13 @@ mod property_tests { use super::*; use proptest::prelude::*; + /// A generated name is only a variable name if the lexer reads it as an + /// identifier: `[a-z]{3,8}` also yields keywords such as `fun` or `let`. + fn is_plain_identifier(name: &str) -> bool { + let mut tokens = ruchy::TokenStream::new(name); + matches!(tokens.next(), Some((ruchy::Token::Identifier(_), _))) && tokens.next().is_none() + } + /// Property: Any global variable assignment should not deadlock /// Strategy: Generate random variable names and values #[test] @@ -276,6 +283,7 @@ mod property_tests { init_val in 0i32..100, op_val in 1i32..50, )| { + prop_assume!(is_plain_identifier(&var_name)); let code = format!( r#" let mut {var_name} = {init_val} @@ -341,6 +349,7 @@ println!("{{}}", {var_name}) var_name in "[a-z]{3,8}", init_val in 0i32..100, )| { + prop_assume!(is_plain_identifier(&var_name)); // Use fixed value to avoid return type inference bug let code = format!( r#" From 1ba0c03c02ea42139046b4f50e39c6061d10ff05 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:51:42 +0200 Subject: [PATCH 093/106] RHLGA-1: NESTARRVEC-1 inner array literals of an index-grown binding are vec! Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 + docs/roadmaps/roadmap.yaml | 4 +- src/backend/transpiler/bindings.rs | 59 ++++++-- src/backend/transpiler/mutation_detection.rs | 51 ++++++- tests/g2b_transfix_5.rs | 144 +++++++++++++++++++ 5 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 tests/g2b_transfix_5.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d04a59f09..7d45f13f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -155,6 +155,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. arguments is an error in the interpreter, as rustc rejects it: `println("{}", 1, 2)` and `println("{0} {0}", 1, 2)` fail with "argument never used"; an explicit `{1}` counts as a use (FMTEXTRA-1). +- A nested array literal whose elements are grown through an index + (`m[0].push(9)`, `m[i].insert(0, x)`) is transpiled with `vec![..]` inner + literals, so `let mut m = [[1], [2]]; m[0].push(9)` compiles and prints + `[[1, 9], [2]]` instead of failing rustc E0599 (NESTARRVEC-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 320994ae7..447110315 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4718,11 +4718,11 @@ roadmap: github_issue: null item_type: task title: 'NESTARRVEC-1: a nested array literal transpiles to a fixed-size array, so m[i].push(x) fails rustc E0599' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-24T14:30:00Z - updated: 2026-09-24T14:30:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let mut m = [[1], [2]]; m[0].push(9); println("{:?}", m)' diff --git a/src/backend/transpiler/bindings.rs b/src/backend/transpiler/bindings.rs index a59bcf6b6..9b783e87a 100644 --- a/src/backend/transpiler/bindings.rs +++ b/src/backend/transpiler/bindings.rs @@ -72,6 +72,9 @@ impl Transpiler { // RHLGA-1 F7: `safe_ident` never r#-escapes `self`/`Self`/`super`/`crate`. let name_ident = Self::safe_ident(name); self.track_string_binding(name, value, None); + // NESTARRVEC-1: inner array literals of a binding whose elements grow + let inner_vec_value = elem_grown_value(name, value, body); + let value = inner_vec_value.as_ref().unwrap_or(value); // Auto-detect mutability let effective_mutability = is_mutable @@ -566,7 +569,9 @@ impl Transpiler { /// LETVEC-1: a copy of the statement-level `let` `expr` with its non-empty /// array literal replaced by `vec![..]`, when a later sibling in `rest` is a -/// Vec-only method call on the binding; `None` otherwise. +/// Vec-only method call on the binding; `None` otherwise. NESTARRVEC-1: when +/// a sibling grows an element (`m[i].push(x)`), the inner literals become +/// `vec![..]` too. fn grown_let_as_vec(expr: &Expr, rest: &[Expr]) -> Option { let ExprKind::Let { name, value, body, .. @@ -574,26 +579,60 @@ fn grown_let_as_vec(expr: &Expr, rest: &[Expr]) -> Option { else { return None; }; - let ExprKind::List(items) = &value.kind else { - return None; - }; + let is_list = matches!(&value.kind, ExprKind::List(items) if !items.is_empty()); let is_statement = matches!(body.kind, ExprKind::Literal(Literal::Unit)); + if !is_list || !is_statement { + return None; + } let grown = super::mutation_detection::is_grown_in_statements(name, rest); - if items.is_empty() || !is_statement || !grown { + let elem_grown = super::mutation_detection::is_elem_grown_in_statements(name, rest); + if !grown && !elem_grown { return None; } - let mut vec_value = (**value).clone(); - vec_value.kind = ExprKind::MacroInvocation { - name: "vec".to_string(), - args: items.clone(), + let inner = if elem_grown { + inner_lists_as_vec(value) + } else { + (**value).clone() }; let mut rewritten = expr.clone(); if let ExprKind::Let { value, .. } = &mut rewritten.kind { - **value = vec_value; + **value = if grown { list_as_vec(&inner) } else { inner }; } Some(rewritten) } +/// NESTARRVEC-1: `value` with its inner array literals as `vec![..]`, when +/// `value` is an array literal and `body` grows an element of `name`. +fn elem_grown_value(name: &str, value: &Expr, body: &Expr) -> Option { + let is_list = matches!(&value.kind, ExprKind::List(items) if !items.is_empty()); + (is_list && super::mutation_detection::is_elem_grown_as_vec(name, body)) + .then(|| inner_lists_as_vec(value)) +} + +/// NESTARRVEC-1: a copy of the array literal `value` with each element that +/// is itself an array literal replaced by `vec![..]`. +fn inner_lists_as_vec(value: &Expr) -> Expr { + let mut rewritten = value.clone(); + if let ExprKind::List(items) = &mut rewritten.kind { + for item in items.iter_mut() { + *item = list_as_vec(item); + } + } + rewritten +} + +/// LETVEC-1: the array literal `list` as `vec![..]`; any other expression unchanged. +fn list_as_vec(list: &Expr) -> Expr { + let mut rewritten = list.clone(); + if let ExprKind::List(items) = &list.kind { + rewritten.kind = ExprKind::MacroInvocation { + name: "vec".to_string(), + args: items.clone(), + }; + } + rewritten +} + /// LETVEC-1: `Vec<..>` annotation (a bare `Vec` or a generic `Vec`). fn is_vec_annotation(ty: &Type) -> bool { match &ty.kind { diff --git a/src/backend/transpiler/mutation_detection.rs b/src/backend/transpiler/mutation_detection.rs index 137985241..a335d8236 100644 --- a/src/backend/transpiler/mutation_detection.rs +++ b/src/backend/transpiler/mutation_detection.rs @@ -54,6 +54,9 @@ pub const VEC_ONLY_METHODS: &[&str] = &[ "extend_from_slice", ]; +/// Which receiver of a [`VEC_ONLY_METHODS`] call counts as growing `name`. +type GrowCall = fn(&str, &Expr) -> bool; + /// LETVEC-1: true when the variable `name` is the receiver of a /// [`VEC_ONLY_METHODS`] call in `expr` that refers to this binding of `name`. /// @@ -61,26 +64,37 @@ pub const VEC_ONLY_METHODS: &[&str] = &[ /// to the outer binding, its body and the block statements after it do not), /// and a closure or nested function with a parameter `name` is skipped. pub fn is_grown_as_vec(name: &str, expr: &Expr) -> bool { - if is_vec_only_call_on(name, expr) { + grown_by(name, expr, is_vec_only_call_on) +} + +/// NESTARRVEC-1: like [`is_grown_as_vec`], but for an element of `name`: true +/// when `name[i]` is the receiver of a [`VEC_ONLY_METHODS`] call in `expr`. +pub fn is_elem_grown_as_vec(name: &str, expr: &Expr) -> bool { + grown_by(name, expr, is_vec_only_call_on_elem) +} + +/// The scope-following scan of [`is_grown_as_vec`] with the call test `is_call`. +fn grown_by(name: &str, expr: &Expr, is_call: GrowCall) -> bool { + if is_call(name, expr) { return true; } match &expr.kind { - ExprKind::Block(exprs) => is_grown_in_statements(name, exprs), + ExprKind::Block(exprs) => grown_in_statements_by(name, exprs, is_call), ExprKind::Let { name: bound, value, else_block, .. } if bound == name => { - is_grown_as_vec(name, value) + grown_by(name, value, is_call) || else_block .as_deref() - .is_some_and(|e| is_grown_as_vec(name, e)) + .is_some_and(|e| grown_by(name, e, is_call)) } _ if binds_param(name, expr) => false, _ => sub_expressions(expr) .into_iter() - .any(|e| is_grown_as_vec(name, e)), + .any(|e| grown_by(name, e, is_call)), } } @@ -98,8 +112,18 @@ fn binds_param(name: &str, expr: &Expr) -> bool { /// LETVEC-1: [`is_grown_as_vec`] over a statement sequence: the statements /// are scanned in order until one of them re-binds `name` with a `let`. pub fn is_grown_in_statements(name: &str, statements: &[Expr]) -> bool { + grown_in_statements_by(name, statements, is_vec_only_call_on) +} + +/// NESTARRVEC-1: [`is_elem_grown_as_vec`] over a statement sequence. +pub fn is_elem_grown_in_statements(name: &str, statements: &[Expr]) -> bool { + grown_in_statements_by(name, statements, is_vec_only_call_on_elem) +} + +/// The statement scan of [`is_grown_in_statements`] with the call test `is_call`. +fn grown_in_statements_by(name: &str, statements: &[Expr], is_call: GrowCall) -> bool { for statement in statements { - if is_grown_as_vec(name, statement) { + if grown_by(name, statement, is_call) { return true; } if matches!(&statement.kind, ExprKind::Let { name: bound, .. } if bound == name) { @@ -121,6 +145,21 @@ fn is_vec_only_call_on(name: &str, expr: &Expr) -> bool { && VEC_ONLY_METHODS.contains(&method.as_str()) } +/// NESTARRVEC-1: `name[i].(..)` with `m` a [`VEC_ONLY_METHODS`] method. +fn is_vec_only_call_on_elem(name: &str, expr: &Expr) -> bool { + let ExprKind::MethodCall { + receiver, method, .. + } = &expr.kind + else { + return false; + }; + let ExprKind::IndexAccess { object, .. } = &receiver.kind else { + return false; + }; + matches!(&object.kind, ExprKind::Identifier(root) if root == name) + && VEC_ONLY_METHODS.contains(&method.as_str()) +} + /// Checks if a variable is mutated (reassigned or modified) in an expression tree /// /// This function traverses the AST recursively to detect writes through a diff --git a/tests/g2b_transfix_5.rs b/tests/g2b_transfix_5.rs new file mode 100644 index 000000000..4a5f1b2d2 --- /dev/null +++ b/tests/g2b_transfix_5.rs @@ -0,0 +1,144 @@ +//! RHLGA-1 transpiler fixes (fifth batch), each checked end to end: +//! `ruchy transpile`, `ruchy compile` (rustc), run the binary, compare exact +//! stdout, and require `ruchy run` to print the same. +//! +//! - NESTARRVEC-1: when an element of a nested array literal binding is +//! grown through an index (`m[0].push(9)`), the inner literals are emitted +//! as `vec![..]` too. Before, they stayed fixed-size arrays (E0599). + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +/// (stdout, stderr) of a command that must succeed. +fn output_of(mut cmd: Command, what: &str) -> (String, String) { + let out = cmd.output().expect("spawn command"); + let stdout = String::from_utf8(out.stdout).expect("utf8 stdout"); + let stderr = String::from_utf8(out.stderr).expect("utf8 stderr"); + assert!(out.status.success(), "{what} failed:\n{stdout}{stderr}"); + (stdout, stderr) +} + +fn transpile(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + output_of(cmd, &format!("ruchy transpile of:\n{src}\n")).0 +} + +/// `ruchy compile` the source, run the binary, return (stdout, stderr). +fn compile_and_run(src: &str) -> (String, String) { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let bin = dir.path().join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(90)); + output_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + output_of(run, "compiled binary") +} + +fn interpret(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + output_of(cmd, &format!("ruchy run of:\n{src}\n")).0 +} + +fn main_with(body: &str) -> String { + format!("fun main() {{\n{body}\n}}\n") +} + +/// Compiled stdout is `expected`; with `interp`, `ruchy run` agrees. +fn check(src: &str, expected: &str, interp: bool) { + let (stdout, _) = compile_and_run(src); + assert_eq!( + stdout, + expected, + "compiled stdout of:\n{src}\n{}", + transpile(src) + ); + if interp { + assert_eq!(interpret(src), expected, "interpreter stdout of:\n{src}"); + } +} + +// -------------------------------------------------------------- NESTARRVEC-1 + +#[test] +fn test_nestarrvec_1_push_through_index() { + let src = main_with( + r#" let mut m = [[1], [2]] + m[0].push(9) + println("{:?}", m)"#, + ); + check(&src, "[[1, 9], [2]]\n", true); +} + +#[test] +fn test_nestarrvec_1_insert_through_index() { + let src = main_with( + r#" let mut m = [[1, 2], [3]] + m[1].insert(0, 7) + println("{:?}", m)"#, + ); + check(&src, "[[1, 2], [7, 3]]\n", true); +} + +#[test] +fn test_nestarrvec_1_push_in_loop() { + let src = main_with( + r#" let mut m = [[0], [0], [0]] + for i in 0..3 { + m[i].push(i) + } + println("{:?}", m)"#, + ); + check(&src, "[[0, 0], [0, 1], [0, 2]]\n", true); +} + +#[test] +fn test_nestarrvec_1_outer_and_inner_grown() { + let src = main_with( + r#" let mut m = [[1]] + m[0].push(5) + m.push(m[0].clone()) + println("{:?}", m)"#, + ); + // `ruchy run` has no `clone` array method ("Unknown array method: + // clone"): an interpreter defect outside this transpiler fix. + check(&src, "[[1, 5], [1, 5]]\n", false); +} + +#[test] +fn test_nestarrvec_1_ungrown_nested_stays_array() { + let src = main_with( + r#" let m = [[1], [2]] + println("{:?}", m)"#, + ); + let rust = transpile(&src); + assert!( + !rust.contains("vec !"), + "no vec! for an ungrown literal:\n{rust}" + ); + check(&src, "[[1], [2]]\n", true); +} From 3c85d1a32b7a76b0cd11d079df02398882d56189 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:52:57 +0200 Subject: [PATCH 094/106] RHLGA-1: CLOSUREUNIT-1 a unit closure call as main's last statement is not printed Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 + docs/roadmaps/roadmap.yaml | 4 +- src/backend/transpiler/function_transpiler.rs | 61 +++++++++++++++++- tests/g2b_transfix_5.rs | 63 +++++++++++++++++++ 4 files changed, 126 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d45f13f8..f709c35f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,6 +159,9 @@ Behaviour changes a program can observe are marked **(behaviour)**. (`m[0].push(9)`, `m[i].insert(0, x)`) is transpiled with `vec![..]` inner literals, so `let mut m = [[1], [2]]; m[0].push(9)` compiles and prints `[[1, 9], [2]]` instead of failing rustc E0599 (NESTARRVEC-1). +- A call to a let-bound closure whose body is unit, as the last statement + of main, is no longer printed: `let f = |n| println(n); f(7)` prints `7`, + not `7` then `()` (CLOSUREUNIT-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 447110315..2f12dc59a 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4737,11 +4737,11 @@ roadmap: github_issue: null item_type: task title: 'CLOSUREUNIT-1: a unit-returning closure call as the last statement of main is printed as () (MAINUNIT-1 covers user functions only)' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-24T14:30:00Z - updated: 2026-09-24T14:30:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let f = |s| println(s); f("hi") as the last statement of main' diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index c740813bd..558225107 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -124,14 +124,20 @@ impl Transpiler { } /// BOOK-COMPAT-015: Check if body has a non-unit last expression - /// (MAINUNIT-1: a call to a unit-returning user function is unit) + /// (MAINUNIT-1: a call to a unit-returning user function is unit; + /// CLOSUREUNIT-1: so is a call to a let-bound closure with a unit body) fn has_non_unit_last_expr(&self, body: &Expr) -> bool { + self.has_non_unit_tail(body) && !tail_calls_unit_closure(body) + } + + /// BOOK-COMPAT-015: the last expression of `body` is not unit. + fn has_non_unit_tail(&self, body: &Expr) -> bool { match &body.kind { ExprKind::Block(exprs) => exprs .last() - .is_some_and(|last| self.has_non_unit_last_expr(last)), + .is_some_and(|last| self.has_non_unit_tail(last)), ExprKind::Let { body, .. } | ExprKind::LetPattern { body, .. } => { - self.has_non_unit_last_expr(body) + self.has_non_unit_tail(body) } _ => !Self::is_unit_expr(body) && !self.is_unit_function_call(body), } @@ -310,6 +316,55 @@ impl Transpiler { } } +/// CLOSUREUNIT-1: the last expression of `body` calls a closure bound by a +/// `let` earlier in `body` whose body is unit (the latest binding of the +/// name wins, so a shadowing value closure is still printed). +fn tail_calls_unit_closure(body: &Expr) -> bool { + let mut closures = std::collections::HashMap::new(); + tail_with_closures(body, &mut closures) +} + +/// CLOSUREUNIT-1: walk the statement chain of `expr` to its tail, recording +/// in `closures` whether each let-bound name is a unit-bodied closure. +fn tail_with_closures<'a>( + expr: &'a Expr, + closures: &mut std::collections::HashMap<&'a str, bool>, +) -> bool { + match &expr.kind { + ExprKind::Block(exprs) => { + let Some((last, init)) = exprs.split_last() else { + return false; + }; + init.iter() + .for_each(|e| record_closure_binding(e, closures)); + tail_with_closures(last, closures) + } + ExprKind::Let { body, .. } => { + record_closure_binding(expr, closures); + tail_with_closures(body, closures) + } + ExprKind::Call { func, .. } => matches!( + &func.kind, + ExprKind::Identifier(name) if closures.get(name.as_str()) == Some(&true) + ), + _ => false, + } +} + +/// CLOSUREUNIT-1: record whether the `let` `expr` binds a unit-bodied closure. +fn record_closure_binding<'a>( + expr: &'a Expr, + closures: &mut std::collections::HashMap<&'a str, bool>, +) { + if let ExprKind::Let { name, value, .. } = &expr.kind { + let is_unit_closure = matches!( + &value.kind, + ExprKind::Lambda { body, .. } if super::function_analysis::is_void_expression(body) + ); + closures.insert(name.as_str(), is_unit_closure); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/g2b_transfix_5.rs b/tests/g2b_transfix_5.rs index 4a5f1b2d2..056f58b31 100644 --- a/tests/g2b_transfix_5.rs +++ b/tests/g2b_transfix_5.rs @@ -5,6 +5,11 @@ //! - NESTARRVEC-1: when an element of a nested array literal binding is //! grown through an index (`m[0].push(9)`), the inner literals are emitted //! as `vec![..]` too. Before, they stayed fixed-size arrays (E0599). +//! - CLOSUREUNIT-1: a call to a let-bound closure whose body is unit, as the +//! last statement of main, is not printed. Before, main printed `()`. +//! (An untyped parameter printed with `println(s)` still uses `{:?}`, so +//! `f("hi")` through `|s|` prints `"hi"`: a separate defect, also hit by +//! `fun p(s)`; these tests type the parameter or pass an integer.) use assert_cmd::Command; use std::path::Path; @@ -142,3 +147,61 @@ fn test_nestarrvec_1_ungrown_nested_stays_array() { ); check(&src, "[[1], [2]]\n", true); } + +// ------------------------------------------------------------- CLOSUREUNIT-1 + +#[test] +fn test_closureunit_1_unit_closure_last_in_main() { + let src = main_with( + r#" let f = |s: String| println(s) + f("hi")"#, + ); + check(&src, "hi\n", true); +} + +#[test] +fn test_closureunit_1_untyped_int_closure_last_in_main() { + let src = main_with( + r#" let f = |n| println(n) + f(7)"#, + ); + check(&src, "7\n", true); +} + +#[test] +fn test_closureunit_1_shadowed_by_value_closure_still_printed() { + let src = main_with( + r#" let f = |n| println(n) + f(1) + let f = |n| n * 3 + f(2)"#, + ); + check(&src, "1\n6\n", false); +} + +#[test] +fn test_closureunit_1_unit_closure_last_top_level() { + let src = "let f = |s: String| println(s)\nf(\"hi\")\n"; + check(src, "hi\n", true); +} + +#[test] +fn test_closureunit_1_block_body_closure_last() { + let src = main_with( + r#" let g = |n| { + let d = n * 2 + println(d) + } + g(4)"#, + ); + check(&src, "8\n", true); +} + +#[test] +fn test_closureunit_1_value_closure_still_printed() { + let src = main_with( + r#" let h = |n| n + 1 + h(41)"#, + ); + check(&src, "42\n", false); +} From 9f2d58d5012bc5a6c0eeefa543506378dd460f15 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:56:33 +0200 Subject: [PATCH 095/106] RHLGA-1: FORLEAK-1 a for loop binds its pattern in a per-iteration scope The for loop assigned its variable with set_variable, which overwrote a same-named outer binding, and ignored the parsed pattern, so for (k, v) bound k to the whole tuple. Each iteration now pushes a scope, binds the pattern (try_pattern_match) there, runs the body and pops the scope. if-let, while-let and match arms did not leak; a regression test pins that. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 5 + docs/roadmaps/roadmap.yaml | 4 +- src/runtime/interpreter_control_flow.rs | 78 ++++++++++--- tests/g2b_runfix_5.rs | 146 ++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 15 deletions(-) create mode 100644 tests/g2b_runfix_5.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f709c35f3..14e8db145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,6 +162,11 @@ Behaviour changes a program can observe are marked **(behaviour)**. - A call to a let-bound closure whose body is unit, as the last statement of main, is no longer printed: `let f = |n| println(n); f(7)` prints `7`, not `7` then `()` (CLOSUREUNIT-1). +- **(behaviour)** A `for` loop binds its variable or pattern in a scope of + its own for each iteration: `let x = "a"; for x in [[1]] { }; println(x)` + prints `a`, not `[1]`, and `for (k, v) in pairs` binds `k` and `v` to the + tuple's fields instead of `k` to the whole tuple. Assignments to outer + variables in the body still persist (FORLEAK-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 2f12dc59a..7b66c5622 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4680,11 +4680,11 @@ roadmap: github_issue: null item_type: task title: 'FORLEAK-1: interpreter for-loop variable leaks into the enclosing scope and overwrites a same-named outer binding' - status: planned + status: completed priority: critical assigned_to: null created: 2026-09-24T14:30:00Z - updated: 2026-09-24T14:30:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let x = "a"; for x in [[1]] { }; println(x) prints [1]' diff --git a/src/runtime/interpreter_control_flow.rs b/src/runtime/interpreter_control_flow.rs index 71fee237f..87e145ba4 100644 --- a/src/runtime/interpreter_control_flow.rs +++ b/src/runtime/interpreter_control_flow.rs @@ -16,25 +16,40 @@ use crate::runtime::interpreter::{Interpreter, LoopControlOrError}; use crate::runtime::{InterpreterError, Value}; use std::sync::Arc; +/// The names a `for` loop binds per element: its pattern, or (for callers +/// without a parsed pattern) a single variable. +#[derive(Clone, Copy)] +pub(crate) struct ForBinding<'a> { + pub(crate) var: &'a str, + pub(crate) pattern: Option<&'a Pattern>, +} + +impl<'a> From<&'a str> for ForBinding<'a> { + fn from(var: &'a str) -> Self { + Self { var, pattern: None } + } +} + impl Interpreter { /// Evaluate a for loop pub(crate) fn eval_for_loop( &mut self, label: Option<&String>, var: &str, - _pattern: Option<&Pattern>, + pattern: Option<&Pattern>, iter: &Expr, body: &Expr, ) -> Result { let iter_value = self.eval_expr(iter)?; + let binding = ForBinding { var, pattern }; match iter_value { - Value::Array(ref arr) => self.eval_for_array_iteration(label, var, arr, body), + Value::Array(ref arr) => self.eval_for_array_iteration(label, binding, arr, body), Value::Range { ref start, ref end, inclusive, - } => self.eval_for_range_iteration(label, var, start, end, inclusive, body), + } => self.eval_for_range_iteration(label, binding, start, end, inclusive, body), _ => Err(InterpreterError::TypeError( "For loop requires an iterable".to_string(), )), @@ -46,15 +61,14 @@ impl Interpreter { pub(crate) fn eval_for_array_iteration( &mut self, label: Option<&String>, - loop_var: &str, + binding: ForBinding<'_>, arr: &[Value], body: &Expr, ) -> Result { let mut last_value = Value::nil(); for item in arr { - self.set_variable(loop_var, item.clone()); - match self.eval_loop_body_with_control_flow(body) { + match self.eval_for_iteration(binding, item.clone(), body) { Ok(value) => last_value = value, Err(LoopControlOrError::Break(break_label, break_val)) => { // If break has no label or matches this loop's label, break here @@ -90,7 +104,7 @@ impl Interpreter { pub(crate) fn eval_for_range_iteration( &mut self, label: Option<&String>, - loop_var: &str, + binding: ForBinding<'_>, start: &Value, end: &Value, inclusive: bool, @@ -100,8 +114,7 @@ impl Interpreter { let mut last_value = Value::nil(); for i in self.create_range_iterator(start_val, end_val, inclusive) { - self.set_variable(loop_var, Value::Integer(i)); - match self.eval_loop_body_with_control_flow(body) { + match self.eval_for_iteration(binding, Value::Integer(i), body) { Ok(value) => last_value = value, Err(LoopControlOrError::Break(break_label, break_val)) => { if break_label.is_none() || break_label.as_deref() == label.map(String::as_str) @@ -128,6 +141,45 @@ impl Interpreter { Ok(last_value) } + /// FORLEAK-1: one iteration of a `for` loop. The loop pattern is bound in + /// a scope of its own, so a same-named outer binding is shadowed, not + /// overwritten; assignments to outer variables in the body still reach + /// them. + fn eval_for_iteration( + &mut self, + binding: ForBinding<'_>, + item: Value, + body: &Expr, + ) -> Result { + self.push_scope(); + let result = self + .bind_for_item(binding, item) + .map_err(LoopControlOrError::Error) + .and_then(|()| self.eval_loop_body_with_control_flow(body)); + self.pop_scope(); + result + } + + /// Bind one loop element to the loop pattern in the current scope. + fn bind_for_item( + &mut self, + binding: ForBinding<'_>, + item: Value, + ) -> Result<(), InterpreterError> { + let bindings = match binding.pattern { + Some(pattern) => self.try_pattern_match(pattern, &item)?.ok_or_else(|| { + InterpreterError::RuntimeError(format!( + "for-loop pattern does not match element {item}" + )) + })?, + None => vec![(binding.var.to_string(), item)], + }; + for (name, value) in bindings { + self.env_set(name, value); + } + Ok(()) + } + /// Extract integer bounds from range values /// Complexity: ≤3 pub(crate) fn extract_range_bounds( @@ -713,7 +765,7 @@ mod tests { let body = make_expr(ExprKind::Literal(Literal::Integer(42, None))); let result = interp - .eval_for_array_iteration(None, "x", &[], &body) + .eval_for_array_iteration(None, "x".into(), &[], &body) .unwrap(); assert_eq!(result, Value::nil()); } @@ -728,7 +780,7 @@ mod tests { let body = make_expr(ExprKind::Identifier("x".to_string())); let result = interp - .eval_for_array_iteration(None, "x", &arr, &body) + .eval_for_array_iteration(None, "x".into(), &arr, &body) .unwrap(); // Last value is 3 assert_eq!(result, Value::Integer(3)); @@ -747,7 +799,7 @@ mod tests { let body = make_expr(ExprKind::Identifier("i".to_string())); let result = interp - .eval_for_range_iteration(None, "i", &start, &end, false, &body) + .eval_for_range_iteration(None, "i".into(), &start, &end, false, &body) .unwrap(); // Last value is 2 (exclusive) assert_eq!(result, Value::Integer(2)); @@ -762,7 +814,7 @@ mod tests { let body = make_expr(ExprKind::Identifier("i".to_string())); let result = interp - .eval_for_range_iteration(None, "i", &start, &end, true, &body) + .eval_for_range_iteration(None, "i".into(), &start, &end, true, &body) .unwrap(); // Last value is 3 (inclusive) assert_eq!(result, Value::Integer(3)); diff --git a/tests/g2b_runfix_5.rs b/tests/g2b_runfix_5.rs new file mode 100644 index 000000000..480427528 --- /dev/null +++ b/tests/g2b_runfix_5.rs @@ -0,0 +1,146 @@ +//! RHLGA-1 G2b run fixes, part 5. +//! +//! - FORLEAK-1: a `for` loop bound its variable with an assignment, so a +//! same-named outer binding was overwritten, and a destructuring pattern +//! (`for (k, v) in ...`) bound the whole element to its first name. Each +//! iteration now binds its pattern in a scope of its own. +//! - ARRREPEAT-1: arrays had no `repeat` method (Rust `slice::repeat`). +//! - FORMATVAL-1: `format`/`println` called through a variable or parameter +//! treated a non-literal first argument as a format template; only a +//! string literal is one (FMTTEMPLATE-1). +//! +//! Where the transpiler can build the program, `ruchy run` is compared with +//! the binary `ruchy compile` builds from the same source. + +use assert_cmd::Command; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +fn write_source(dir: &Path, src: &str) -> PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn ruchy() -> Command { + Command::cargo_bin("ruchy").expect("ruchy binary is built for tests") +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn run_cmd(dir: &Path, src: &str) -> Command { + let mut cmd = ruchy(); + cmd.arg("run") + .arg(write_source(dir, src)) + .timeout(Duration::from_secs(10)); + cmd +} + +fn compile_cmd(dir: &Path, src: &str) -> (Command, PathBuf) { + let bin = dir.join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(write_source(dir, src)) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(180)); + (cmd, bin) +} + +fn compiled_run(dir: &Path, src: &str) -> String { + let (cmd, bin) = compile_cmd(dir, src); + stdout_of(cmd, "ruchy compile"); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(10)); + stdout_of(run, "compiled binary") +} + +/// The compiled binary prints `expected`, and `ruchy run` prints it too. +fn assert_run_matches_compiled(src: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!(compiled_run(dir.path(), src), expected, "compiled binary"); + assert_eq!( + stdout_of(run_cmd(dir.path(), src), "ruchy run"), + expected, + "ruchy run" + ); +} + +/// `ruchy run` prints `expected`: the output Rust semantics give. Used where +/// the transpiler cannot build the program yet. +fn assert_run_prints(src: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!( + stdout_of(run_cmd(dir.path(), src), "ruchy run"), + expected, + "ruchy run" + ); +} + +// -------------------------------------------------------------- FORLEAK-1 + +#[test] +fn test_forleak_1_outer_binding_survives_same_named_loop_var() { + assert_run_matches_compiled( + "let x = \"a\"\nfor x in [[1]] { }\nprintln(\"{}\", x)\n", + "a\n", + ); +} + +#[test] +fn test_forleak_1_outer_binding_survives_range_loop_var() { + assert_run_matches_compiled( + "let i = 10\nfor i in 0..3 { println(\"{}\", i) }\nprintln(\"{}\", i)\n", + "0\n1\n2\n10\n", + ); +} + +#[test] +fn test_forleak_1_outer_mutation_in_body_persists() { + assert_run_matches_compiled( + "let mut s = 0\nfor i in [1, 2] { s += i }\nprintln(\"{}\", s)\n", + "3\n", + ); +} + +#[test] +fn test_forleak_1_break_and_continue_still_work() { + assert_run_matches_compiled( + "let mut t = 0\nfor i in 0..10 {\n if i == 1 { continue }\n if i == 4 { break }\n t += i\n}\nprintln(\"{}\", t)\n", + "5\n", + ); +} + +#[test] +fn test_forleak_1_tuple_pattern_binds_each_name() { + assert_run_prints( + "let k = 7\nfor (k, v) in [(1, 2), (3, 4)] { println(\"{} {}\", k, v) }\nprintln(\"{}\", k)\n", + "1 2\n3 4\n7\n", + ); +} + +#[test] +fn test_forleak_1_pattern_binding_leaves_outer_second_name() { + assert_run_prints( + "let v = \"outer\"\nfor (k, v) in [(1, 2)] { println(\"{}\", k + v) }\nprintln(\"{}\", v)\n", + "3\nouter\n", + ); +} + +#[test] +fn test_forleak_1_if_let_while_let_match_do_not_leak() { + assert_run_prints( + "let y = 1\nif let Some(y) = Some(3) { println(\"{}\", y) }\nprintln(\"{}\", y)\n\ + let mut w = Some(4)\nlet q = 1\nwhile let Some(q) = w { println(\"{}\", q)\n w = None }\nprintln(\"{}\", q)\n\ + let z = 1\nmatch 5 { z => println(\"{}\", z) }\nprintln(\"{}\", z)\n", + "3\n1\n4\n1\n5\n1\n", + ); +} From d269a88f871cc53ece1f3f1caa2a530a8b71b69a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:57:25 +0200 Subject: [PATCH 096/106] RHLGA-1: ARRREPEAT-1 arrays have repeat(n) like Rust slices eval_array_repeat clones the elements n times; a negative or non-integer count is a runtime error. Dispatched with the other one-argument array methods in eval_array.rs. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 ++ docs/roadmaps/roadmap.yaml | 4 ++-- src/runtime/eval_array.rs | 16 ++++++++++++++++ tests/g2b_runfix_5.rs | 31 +++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e8db145..77729a2ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,8 @@ Behaviour changes a program can observe are marked **(behaviour)**. prints `a`, not `[1]`, and `for (k, v) in pairs` binds `k` and `v` to the tuple's fields instead of `k` to the whole tuple. Assignments to outer variables in the body still persist (FORLEAK-1). +- Arrays have `repeat(n)`, as Rust slices do: `[1].repeat(2)` is `[1, 1]`; + a negative count is an error (ARRREPEAT-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 7b66c5622..8a519d543 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4699,11 +4699,11 @@ roadmap: github_issue: null item_type: task title: 'ARRREPEAT-1: interpreter arrays have no repeat method (Unknown array method: repeat); Rust slices have repeat' - status: planned + status: completed priority: medium assigned_to: null created: 2026-09-24T14:30:00Z - updated: 2026-09-24T14:30:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let v = [1]; println("{:?}", v.repeat(2))' diff --git a/src/runtime/eval_array.rs b/src/runtime/eval_array.rs index 2d3983882..20be0e51e 100644 --- a/src/runtime/eval_array.rs +++ b/src/runtime/eval_array.rs @@ -61,6 +61,7 @@ fn eval_array_unary_method( // METHODS-1: `drop(n)` is the array without its first n elements "skip" | "drop" => eval_array_skip(arr, arg), "zip" => eval_array_zip(arr, arg), + "repeat" => eval_array_repeat(arr, arg), _ => return None, }; Some(result) @@ -438,6 +439,21 @@ fn eval_array_join(arr: &Arc<[Value]>, separator: &Value) -> Result, count: &Value) -> Result { + let times = match count { + Value::Integer(n) => usize::try_from(*n).ok(), + _ => None, + } + .ok_or_else(|| { + InterpreterError::RuntimeError(format!( + "Array.repeat() expects a non-negative integer, got {count}" + )) + })?; + let items = (0..times).flat_map(|_| arr.iter().cloned()).collect(); + Ok(Value::from_array(items)) +} + /// Remove duplicate elements from array /// Complexity: 3 (within Toyota Way limits) fn eval_array_unique(arr: &Arc<[Value]>) -> Result { diff --git a/tests/g2b_runfix_5.rs b/tests/g2b_runfix_5.rs index 480427528..7e9d7b519 100644 --- a/tests/g2b_runfix_5.rs +++ b/tests/g2b_runfix_5.rs @@ -144,3 +144,34 @@ fn test_forleak_1_if_let_while_let_match_do_not_leak() { "3\n1\n4\n1\n5\n1\n", ); } + +// ------------------------------------------------------------ ARRREPEAT-1 + +#[test] +fn test_arrrepeat_1_repeat_twice() { + assert_run_matches_compiled("let v = [1]\nprintln(\"{:?}\", v.repeat(2))\n", "[1, 1]\n"); +} + +#[test] +fn test_arrrepeat_1_repeat_keeps_order_and_zero_is_empty() { + assert_run_prints( + "let v = [1, 2]\nprintln(\"{:?}\", v.repeat(3))\nprintln(\"{:?}\", v.repeat(0))\nprintln(\"{:?}\", v)\n", + "[1, 2, 1, 2, 1, 2]\n[]\n[1, 2]\n", + ); +} + +#[test] +fn test_arrrepeat_1_negative_count_is_error() { + let dir = tempfile::tempdir().expect("tempdir"); + let out = run_cmd(dir.path(), "let v = [1]\nprintln(\"{:?}\", v.repeat(-1))\n") + .output() + .expect("spawn ruchy run"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(!out.status.success(), "ruchy run accepted: {stderr}"); + assert!(stderr.contains("repeat"), "stderr lacks repeat: {stderr}"); +} + +#[test] +fn test_arrrepeat_1_string_repeat_unchanged() { + assert_run_prints("println(\"{}\", \"ab\".repeat(2))\n", "abab\n"); +} From 1253fbbfe80abfe2b6177a7abb76d37ef48a1245 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 12:57:57 +0200 Subject: [PATCH 097/106] RHLGA-1: FORMATVAL-1 a format builtin called by name follows the literal-template rule format_sink_for now routes any identifier bound to a __builtin_*__ format marker (println, let g = format, a parameter holding one) through eval_format_call, the single FMTTEMPLATE-1 implementation, so only a string-literal first argument is a template. The value-only path (eval_builtin_format) keeps its heuristic for callees that are not identifiers, where no argument expressions exist. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 ++++ docs/roadmaps/roadmap.yaml | 4 ++-- src/runtime/interpreter_format_calls.rs | 14 +++++++---- tests/g2b_runfix_5.rs | 31 +++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77729a2ba..f623205ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,6 +169,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. variables in the body still persist (FORLEAK-1). - Arrays have `repeat(n)`, as Rust slices do: `[1].repeat(2)` is `[1, 1]`; a negative count is an error (ARRREPEAT-1). +- **(behaviour)** `format`, `println`, `print`, `eprintln` and `eprint` + called through a variable or parameter (`let g = format; g(t, 1)`) follow + the FMTTEMPLATE-1 rule: only a string literal first argument is a + template, so a non-literal one prints `{} 1` (FORMATVAL-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 8a519d543..aed6d2308 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4756,11 +4756,11 @@ roadmap: github_issue: null item_type: task title: 'FORMATVAL-1: the format builtin called as a value (eval_builtin.rs:318) still treats a non-literal first argument as a template when args.len() > 1' - status: planned + status: completed priority: medium assigned_to: null created: 2026-09-24T14:30:00Z - updated: 2026-09-24T14:30:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let t = "{}"; let g = format; println(g(t, 1))' diff --git a/src/runtime/interpreter_format_calls.rs b/src/runtime/interpreter_format_calls.rs index b5a8f2276..dbe505f1b 100644 --- a/src/runtime/interpreter_format_calls.rs +++ b/src/runtime/interpreter_format_calls.rs @@ -42,6 +42,7 @@ impl FormatSink { Self::for_name(name) } + #[cfg(test)] fn marker(self) -> &'static str { match self { Self::Print => "__builtin_print__", @@ -78,8 +79,10 @@ fn format_error(e: String) -> InterpreterError { InterpreterError::RuntimeError(format!("format error: {e}")) } -/// The builtin path (no scope): `println` called through a value. The first -/// value is a format string when more values follow it. +/// The builtin path (no scope and no argument expressions): a format builtin +/// called where only values exist, e.g. from a non-identifier callee. Calls +/// through a name take `Interpreter::eval_format_call` (FORMATVAL-1); here +/// the first value is a format string when more values follow it. pub fn eval_builtin_format(sink: FormatSink, args: &[Value]) -> Result { let template = args.len() > 1; let text = fmt_spec::format_call(args, template, &|_| None).map_err(format_error)?; @@ -117,13 +120,16 @@ impl Interpreter { } } + /// FORMATVAL-1: any identifier bound to an output/format builtin + /// (`println`, or `g` after `let g = format`, or a parameter holding + /// one) formats with the argument expressions, so the FMTTEMPLATE-1 + /// literal rule applies however the builtin is reached by name. fn format_sink_for(&self, func: &Expr) -> Option { let ExprKind::Identifier(name) = &func.kind else { return None; }; - let sink = FormatSink::for_name(name)?; match self.lookup_variable(name) { - Ok(Value::String(marker)) if marker.as_ref() == sink.marker() => Some(sink), + Ok(Value::String(marker)) => FormatSink::for_marker(&marker), _ => None, } } diff --git a/tests/g2b_runfix_5.rs b/tests/g2b_runfix_5.rs index 7e9d7b519..6b31e6515 100644 --- a/tests/g2b_runfix_5.rs +++ b/tests/g2b_runfix_5.rs @@ -175,3 +175,34 @@ fn test_arrrepeat_1_negative_count_is_error() { fn test_arrrepeat_1_string_repeat_unchanged() { assert_run_prints("println(\"{}\", \"ab\".repeat(2))\n", "abab\n"); } + +// ------------------------------------------------------------ FORMATVAL-1 + +#[test] +fn test_formatval_1_format_value_non_literal_is_not_template() { + assert_run_prints( + "let t = \"{}\"\nlet g = format\nprintln(\"{}\", g(t, 1))\n", + "{} 1\n", + ); +} + +#[test] +fn test_formatval_1_format_value_literal_is_template() { + assert_run_prints( + "let g = format\nprintln(\"{}\", g(\"{}-{}\", 1, 2))\n", + "1-2\n", + ); +} + +#[test] +fn test_formatval_1_println_value_non_literal_is_not_template() { + assert_run_prints("let t = \"{}\"\nlet p = println\np(t, 1)\n", "{} 1\n"); +} + +#[test] +fn test_formatval_1_format_parameter_follows_literal_rule() { + assert_run_prints( + "fun h(f, t) {\n println(\"{}\", f(t, 1))\n println(\"{}\", f(\"{}!\", 2))\n}\nh(format, \"{}\")\n", + "{} 1\n2!\n", + ); +} From 02cab5680bfe2c05fb31414a6bad7b276bd82c38 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 13:01:14 +0200 Subject: [PATCH 098/106] RHLGA-1: file PRINTPARAM, ARRCLONE, NESTPUSHLIT (found by round-5 workers) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index aed6d2308..adb917e18 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4771,3 +4771,60 @@ roadmap: labels: - kind:code - runtime +- id: PRINTPARAM-1 + github_issue: null + item_type: task + title: 'PRINTPARAM-1: an untyped function or closure parameter inferred as String is printed by println with {:?}, so the compiled program prints "hi" with quotes' + status: planned + priority: high + assigned_to: null + created: 2026-09-24T16:00:00Z + updated: 2026-09-24T16:00:00Z + spec: null + acceptance_criteria: + - 'Repro: fun p(s) { println(s) }; p("hi") and let f = |s| println(s); f("hi")' + - 'Acceptance: compiled prints hi without quotes, like ruchy run' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler +- id: ARRCLONE-1 + github_issue: null + item_type: task + title: 'ARRCLONE-1: interpreter arrays have no clone method (Unknown array method: clone)' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T16:00:00Z + updated: 2026-09-24T16:00:00Z + spec: null + acceptance_criteria: + - 'Repro: let a = [1]; let b = a.clone(); println("{:?}", b)' + - 'Acceptance: run prints [1] like the compiled binary' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime +- id: NESTPUSHLIT-1 + github_issue: null + item_type: task + title: 'NESTPUSHLIT-1: pushing an array literal into a list whose inner elements became vec! (NESTARRVEC-1) fails rustc E0308' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T16:00:00Z + updated: 2026-09-24T16:00:00Z + spec: null + acceptance_criteria: + - 'Repro: let mut m = [[1]]; m[0].push(2); m.push([3]); println("{:?}", m)' + - 'Acceptance: compiled prints [[1, 2], [3]] like ruchy run' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler From aaa7b5fa60a62642237367f81830e8636caf7379 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 13:21:40 +0200 Subject: [PATCH 099/106] RHLGA-1: PRINTPARAM-1 an untyped parameter emitted as a string prints with Display - A named function's parameters whose emitted (inferred) type is String/&str are recorded as strings, so println(s) uses {} (track_emitted_string_params). - A closure bound by let takes its untyped parameters' call-site types (call_site_arg_types) for the same choice (with_closure_param_scope). - Parameters of unknown or non-string type keep {:?}. - tests/g2b_transfix_6.rs: 7 end-to-end tests (compile, run, ruchy run). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 + docs/roadmaps/roadmap.yaml | 4 +- src/backend/transpiler/bindings.rs | 15 +- src/backend/transpiler/function_transpiler.rs | 2 + src/backend/transpiler/lambda_transpiler.rs | 15 +- src/backend/transpiler/print_helpers.rs | 51 +++++- tests/g2b_transfix_6.rs | 145 ++++++++++++++++++ 7 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 tests/g2b_transfix_6.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f623205ec..0b8c79b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,6 +173,9 @@ Behaviour changes a program can observe are marked **(behaviour)**. called through a variable or parameter (`let g = format; g(t, 1)`) follow the FMTTEMPLATE-1 rule: only a string literal first argument is a template, so a non-literal one prints `{} 1` (FORMATVAL-1). +- An untyped function or closure parameter whose emitted type is `String` + or `&str` (a closure's from its call sites) prints with `{}`: + `fun p(s) { println(s) }; p("hi")` prints `hi`, not `"hi"` (PRINTPARAM-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index adb917e18..001499126 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4775,11 +4775,11 @@ roadmap: github_issue: null item_type: task title: 'PRINTPARAM-1: an untyped function or closure parameter inferred as String is printed by println with {:?}, so the compiled program prints "hi" with quotes' - status: planned + status: completed priority: high assigned_to: null created: 2026-09-24T16:00:00Z - updated: 2026-09-24T16:00:00Z + updated: 2026-09-24T20:00:00Z spec: null acceptance_criteria: - 'Repro: fun p(s) { println(s) }; p("hi") and let f = |s| println(s); f("hi")' diff --git a/src/backend/transpiler/bindings.rs b/src/backend/transpiler/bindings.rs index 9b783e87a..d1abd421a 100644 --- a/src/backend/transpiler/bindings.rs +++ b/src/backend/transpiler/bindings.rs @@ -85,7 +85,7 @@ impl Transpiler { let (value_tokens, needs_vec_type_hint) = match &value.kind { ExprKind::Literal(Literal::String(s)) => (quote! { #s.to_string() }, false), ExprKind::List(items) if items.is_empty() => (self.transpile_expr(value)?, true), - _ => (self.transpile_expr(value)?, false), + _ => (self.transpile_let_value(name, value)?, false), }; let value_tokens = Self::growable_list_tokens(name, value, None, body, value_tokens); @@ -351,6 +351,17 @@ impl Transpiler { } } + /// PRINTPARAM-1: the value of `let name = value`; a closure value knows + /// the name it is called by. (complexity: 2) + fn transpile_let_value(&self, name: &str, value: &Expr) -> Result { + match &value.kind { + ExprKind::Lambda { params, body } => { + self.transpile_named_lambda(Some(name), params, body) + } + _ => self.transpile_expr(value), + } + } + /// LETVEC-1: transpile one statement of a block. A statement-level `let` /// (Unit body) whose scope is the following siblings `rest` gets its /// array literal emitted as `vec![..]` when a sibling grows the binding. @@ -401,7 +412,7 @@ impl Transpiler { self.string_vars.borrow_mut().insert(name.to_string()); Ok((self.transpile_expr(value)?, false)) } - _ => Ok((self.transpile_expr(value)?, false)), + _ => Ok((self.transpile_let_value(name, value)?, false)), } } diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index 558225107..77a205efd 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -67,6 +67,8 @@ impl Transpiler { } else { self.generate_param_tokens(params, body, name)? }; + // PRINTPARAM-1: an inferred String/&str parameter prints with Display. + self.track_emitted_string_params(¶m_tokens); // Check for #[test] attribute and override return type if found let has_test_attribute = attributes.iter().any(|attr| attr.name == "test"); diff --git a/src/backend/transpiler/lambda_transpiler.rs b/src/backend/transpiler/lambda_transpiler.rs index 8764d1cbf..1bc755ae3 100644 --- a/src/backend/transpiler/lambda_transpiler.rs +++ b/src/backend/transpiler/lambda_transpiler.rs @@ -18,9 +18,22 @@ impl Transpiler { &self, params: &[Param], body: &Expr, + ) -> Result { + self.transpile_named_lambda(None, params, body) + } + + /// PRINTPARAM-1: a closure bound by `let = |..| ..`; its untyped + /// parameters take the call-site types of `name` for print placeholders. + /// Complexity: 3 + pub(crate) fn transpile_named_lambda( + &self, + name: Option<&str>, + params: &[Param], + body: &Expr, ) -> Result { // PRINTSTRSCOPE-1: parameters shadow outer string records. - let body_tokens = self.with_param_scope(params, || self.transpile_expr(body))?; + let body_tokens = + self.with_closure_param_scope(name, params, || self.transpile_expr(body))?; if params.is_empty() { return Ok(quote! { move || #body_tokens }); diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index 369b9654b..2e49ca56e 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -367,13 +367,56 @@ impl Transpiler { /// PRINTSTRSCOPE-1: run `f` in the scope of closure parameters; a /// `String`/`&str`-annotated parameter is a string. (complexity: 1) pub(crate) fn with_param_scope(&self, params: &[Param], f: impl FnOnce() -> T) -> T { + self.with_closure_param_scope(None, params, f) + } + + /// PRINTPARAM-1: [`Self::with_param_scope`] for a closure bound to + /// `closure`: an untyped parameter is a string when every recorded call + /// site passes a string literal at its position. (complexity: 1) + pub(crate) fn with_closure_param_scope( + &self, + closure: Option<&str>, + params: &[Param], + f: impl FnOnce() -> T, + ) -> T { let binders: Vec<(String, bool)> = params .iter() - .map(|p| (p.name(), is_string_annotation(&p.ty))) + .enumerate() + .map(|(i, p)| (p.name(), self.is_string_closure_param(closure, i, p))) .collect(); self.with_string_scope(&binders, f) } + /// PRINTPARAM-1: an annotated parameter is a string by its annotation; an + /// untyped one by the call-site type of `closure` at `index`. (complexity: 3) + fn is_string_closure_param(&self, closure: Option<&str>, index: usize, param: &Param) -> bool { + if !matches!(¶m.ty.kind, TypeKind::Named(n) if n == "_") { + return is_string_annotation(¶m.ty); + } + closure + .and_then(|name| self.get_call_site_param_type(name, index)) + .is_some_and(|ty| ty == STRING_VAR_TYPE) + } + + /// PRINTPARAM-1: record the parameters whose emitted type is `String` or + /// `&str`; `param_tokens` holds one `name: Type` per parameter, so an + /// inferred type counts like an annotation. (complexity: 3) + pub(crate) fn track_emitted_string_params(&self, param_tokens: &[TokenStream]) { + for tokens in param_tokens { + let text = tokens.to_string(); + let Some((name, ty)) = text.split_once(':') else { + continue; + }; + if is_string_type_text(ty.trim()) { + let name = name + .trim() + .trim_start_matches("mut ") + .trim_start_matches("r#"); + self.set_string_var(name, true); + } + } + } + /// PRINTSTR-1: set or clear the string record of `name`. (complexity: 3) fn set_string_var(&self, name: &str, is_string: bool) { let mut types = self.variable_types.borrow_mut(); @@ -422,6 +465,12 @@ impl Transpiler { } } +/// PRINTPARAM-1: the emitted type text `ty` is `String`, `&str` or +/// `&'a str`. (complexity: 3) +fn is_string_type_text(ty: &str) -> bool { + ty == "String" || ty == "& str" || (ty.starts_with("& '") && ty.ends_with(" str")) +} + /// PRINTSTR-1: `ty` is `String`, `str` or a reference to one. (complexity: 3) fn is_string_annotation(ty: &Type) -> bool { match &ty.kind { diff --git a/tests/g2b_transfix_6.rs b/tests/g2b_transfix_6.rs new file mode 100644 index 000000000..ed2677401 --- /dev/null +++ b/tests/g2b_transfix_6.rs @@ -0,0 +1,145 @@ +//! RHLGA-1 transpiler fixes (sixth batch), each checked end to end: +//! `ruchy transpile`, `ruchy compile` (rustc), run the binary, compare exact +//! stdout, and require `ruchy run` to print the same. +//! +//! - PRINTPARAM-1: an untyped function or closure parameter whose emitted +//! (or call-site inferred) type is `String` prints with `{}` through +//! `println(s)`. Before, `p("hi")` and `f("hi")` printed `"hi"` with quotes. + +use assert_cmd::Command; +use std::path::Path; +use std::time::Duration; + +fn ruchy() -> Command { + let mut cmd = Command::cargo_bin("ruchy").expect("ruchy binary is built for tests"); + cmd.timeout(Duration::from_secs(10)); + cmd +} + +fn write_source(dir: &Path, src: &str) -> std::path::PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +/// (stdout, stderr) of a command that must succeed. +fn output_of(mut cmd: Command, what: &str) -> (String, String) { + let out = cmd.output().expect("spawn command"); + let stdout = String::from_utf8(out.stdout).expect("utf8 stdout"); + let stderr = String::from_utf8(out.stderr).expect("utf8 stderr"); + assert!(out.status.success(), "{what} failed:\n{stdout}{stderr}"); + (stdout, stderr) +} + +fn transpile(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("transpile").arg(file); + output_of(cmd, &format!("ruchy transpile of:\n{src}\n")).0 +} + +/// `ruchy compile` the source, run the binary, return (stdout, stderr). +fn compile_and_run(src: &str) -> (String, String) { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let bin = dir.path().join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(&file) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(90)); + output_of(cmd, &format!("ruchy compile of:\n{src}\n")); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(30)); + output_of(run, "compiled binary") +} + +fn interpret(src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let file = write_source(dir.path(), src); + let mut cmd = ruchy(); + cmd.arg("run").arg(file); + output_of(cmd, &format!("ruchy run of:\n{src}\n")).0 +} + +fn main_with(body: &str) -> String { + format!("fun main() {{\n{body}\n}}\n") +} + +/// Compiled stdout is `expected`; with `interp`, `ruchy run` agrees. +fn check(src: &str, expected: &str, interp: bool) { + let (stdout, _) = compile_and_run(src); + assert_eq!( + stdout, + expected, + "compiled stdout of:\n{src}\n{}", + transpile(src) + ); + if interp { + assert_eq!(interpret(src), expected, "interpreter stdout of:\n{src}"); + } +} + +// ------------------------------------------------------------- PRINTPARAM-1 + +#[test] +fn test_printparam_1_named_fn_string_param() { + let src = format!("fun p(s) {{ println(s) }}\n{}", main_with(" p(\"hi\")")); + check(&src, "hi\n", true); +} + +#[test] +fn test_printparam_1_named_fn_mixed_params() { + let src = format!( + "fun p(s, n) {{\n println(s)\n println(n)\n}}\n{}", + main_with(" p(\"a\", 3)") + ); + check(&src, "a\n3\n", true); +} + +#[test] +fn test_printparam_1_named_fn_int_param_unchanged() { + let src = format!("fun p(n) {{ println(n) }}\n{}", main_with(" p(5)")); + check(&src, "5\n", true); +} + +#[test] +fn test_printparam_1_closure_string_param() { + let src = main_with( + r#" let f = |s| println(s) + f("hi")"#, + ); + check(&src, "hi\n", true); +} + +#[test] +fn test_printparam_1_closure_called_twice() { + let src = main_with( + r#" let f = |s| println(s) + f("a") + f("b")"#, + ); + check(&src, "a\nb\n", true); +} + +#[test] +fn test_printparam_1_closure_top_level() { + let src = "let f = |s| println(s)\nf(\"hi\")\n"; + check(src, "hi\n", true); +} + +#[test] +fn test_printparam_1_closure_list_param_keeps_debug() { + let src = main_with( + r#" let f = |v| println(v) + f([1, 2])"#, + ); + let rust = transpile(&src); + assert!( + rust.contains("{:?}"), + "a non-string parameter keeps {{:?}}:\n{rust}" + ); + check(&src, "[1, 2]\n", true); +} From 53bd350ad1aad9112deac82cc6f28037cf06bc01 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 13:23:06 +0200 Subject: [PATCH 100/106] RHLGA-1: NESTPUSHLIT-1 an array literal pushed into a list of vec! literals is a vec! too - transpile_let/transpile_let_with_type record a binding whose inner list literals were emitted as vec! (the NESTARRVEC-1 rewrite) in inner_vec_lists; any other binding of the name clears it, as does a new function. - transpile_method_call emits the element argument of push/insert on such a binding as vec![..], so m[0].push(2); m.push([3]) compiles. - tests/g2b_transfix_6.rs: 4 end-to-end tests (compile, run, ruchy run). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 ++ docs/roadmaps/roadmap.yaml | 4 +- src/backend/transpiler/bindings.rs | 48 +++++++++++++++++++ src/backend/transpiler/function_transpiler.rs | 2 + src/backend/transpiler/mod.rs | 5 ++ src/backend/transpiler/statements.rs | 4 ++ tests/g2b_transfix_6.rs | 48 +++++++++++++++++++ 7 files changed, 113 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8c79b0d..a571db86c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -176,6 +176,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. - An untyped function or closure parameter whose emitted type is `String` or `&str` (a closure's from its call sites) prints with `{}`: `fun p(s) { println(s) }; p("hi")` prints `hi`, not `"hi"` (PRINTPARAM-1). +- An array literal pushed or inserted into a list whose inner literals + became `vec![..]` (NESTARRVEC-1) is emitted as `vec![..]` too, so + `m[0].push(2); m.push([3])` compiles and prints `[[1, 2], [3]]` + (NESTPUSHLIT-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 001499126..4edf76d8d 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4813,11 +4813,11 @@ roadmap: github_issue: null item_type: task title: 'NESTPUSHLIT-1: pushing an array literal into a list whose inner elements became vec! (NESTARRVEC-1) fails rustc E0308' - status: planned + status: completed priority: medium assigned_to: null created: 2026-09-24T16:00:00Z - updated: 2026-09-24T16:00:00Z + updated: 2026-09-24T20:00:00Z spec: null acceptance_criteria: - 'Repro: let mut m = [[1]]; m[0].push(2); m.push([3]); println("{:?}", m)' diff --git a/src/backend/transpiler/bindings.rs b/src/backend/transpiler/bindings.rs index d1abd421a..5291764b9 100644 --- a/src/backend/transpiler/bindings.rs +++ b/src/backend/transpiler/bindings.rs @@ -75,6 +75,7 @@ impl Transpiler { // NESTARRVEC-1: inner array literals of a binding whose elements grow let inner_vec_value = elem_grown_value(name, value, body); let value = inner_vec_value.as_ref().unwrap_or(value); + self.track_inner_vec_binding(name, value); // Auto-detect mutability let effective_mutability = is_mutable @@ -268,6 +269,7 @@ impl Transpiler { // RHLGA-1 F7: `safe_ident` never r#-escapes `self`/`Self`/`super`/`crate`. let name_ident = Self::safe_ident(name); self.track_string_binding(name, value, type_annotation); + self.track_inner_vec_binding(name, value); // PARSER-073: Generate const/let keyword based on const attribute let is_mutable_var = is_mutable @@ -362,6 +364,39 @@ impl Transpiler { } } + /// NESTPUSHLIT-1: remember whether `name` is bound to a list literal + /// whose inner literals are `vec![..]` (the NESTARRVEC-1 rewrite); any + /// other binding of `name` clears the record. (complexity: 2) + fn track_inner_vec_binding(&self, name: &str, value: &Expr) { + let mut lists = self.inner_vec_lists.borrow_mut(); + if has_vec_inner_literals(value) { + lists.insert(name.to_string()); + } else { + lists.remove(name); + } + } + + /// NESTPUSHLIT-1: the arguments of `object.push(x)` / `object.insert(i, x)` + /// with an array literal `x` as `vec![..]`, when `object` is a binding + /// whose inner literals are `vec![..]`; `None` otherwise. (complexity: 4) + pub(crate) fn inner_vec_push_args( + &self, + object: &Expr, + method: &str, + args: &[Expr], + ) -> Option> { + let ExprKind::Identifier(name) = &object.kind else { + return None; + }; + if !matches!(method, "push" | "insert") || !self.inner_vec_lists.borrow().contains(name) { + return None; + } + let mut rewritten = args.to_vec(); + let element = rewritten.last_mut()?; + *element = list_as_vec(element); + Some(rewritten) + } + /// LETVEC-1: transpile one statement of a block. A statement-level `let` /// (Unit body) whose scope is the following siblings `rest` gets its /// array literal emitted as `vec![..]` when a sibling grows the binding. @@ -632,6 +667,19 @@ fn inner_lists_as_vec(value: &Expr) -> Expr { rewritten } +/// NESTPUSHLIT-1: `value` is a list literal (array or `vec![..]`) with an +/// element that is a `vec![..]` literal. (complexity: 4) +fn has_vec_inner_literals(value: &Expr) -> bool { + let items = match &value.kind { + ExprKind::List(items) => items, + ExprKind::MacroInvocation { name, args } if name == "vec" => args, + _ => return false, + }; + items + .iter() + .any(|item| matches!(&item.kind, ExprKind::MacroInvocation { name, .. } if name == "vec")) +} + /// LETVEC-1: the array literal `list` as `vec![..]`; any other expression unchanged. fn list_as_vec(list: &Expr) -> Expr { let mut rewritten = list.clone(); diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index 77a205efd..6aa16d70d 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -41,6 +41,8 @@ impl Transpiler { // PRINTSTR-1: string parameters print with Display. self.track_string_params(params); + // NESTPUSHLIT-1: nested-vec records of earlier functions end here. + self.inner_vec_lists.borrow_mut().clear(); // TRANSPILER-004 FIX: Track String-typed parameters for proper concat transpilation // Before processing function body, register all String parameters in string_vars diff --git a/src/backend/transpiler/mod.rs b/src/backend/transpiler/mod.rs index 237d8dd4b..e55a10266 100644 --- a/src/backend/transpiler/mod.rs +++ b/src/backend/transpiler/mod.rs @@ -230,6 +230,9 @@ pub struct Transpiler { /// Maps function name to a vector of inferred argument types from call sites. /// Used when function parameters have no explicit type to infer types from usage. pub call_site_arg_types: std::cell::RefCell>>, + /// NESTPUSHLIT-1: bindings whose inner list literals were emitted as + /// `vec![..]` (NESTARRVEC-1); an array literal pushed into one is too. + pub inner_vec_lists: std::cell::RefCell>, } impl Default for Transpiler { fn default() -> Self { @@ -266,6 +269,7 @@ impl Clone for Transpiler { current_struct_name: std::cell::RefCell::new(self.current_struct_name.borrow().clone()), auto_boxed_fields: std::cell::RefCell::new(self.auto_boxed_fields.borrow().clone()), call_site_arg_types: std::cell::RefCell::new(self.call_site_arg_types.borrow().clone()), + inner_vec_lists: std::cell::RefCell::new(self.inner_vec_lists.borrow().clone()), } } } @@ -297,6 +301,7 @@ impl Transpiler { current_struct_name: std::cell::RefCell::new(None), auto_boxed_fields: std::cell::RefCell::new(std::collections::HashMap::new()), call_site_arg_types: std::cell::RefCell::new(std::collections::HashMap::new()), + inner_vec_lists: std::cell::RefCell::new(std::collections::HashSet::new()), } } // EXTREME TDD Round 64: generate_value_printing_tokens moved to print_helpers.rs diff --git a/src/backend/transpiler/statements.rs b/src/backend/transpiler/statements.rs index c460f9fc3..7bc4dc29d 100644 --- a/src/backend/transpiler/statements.rs +++ b/src/backend/transpiler/statements.rs @@ -371,6 +371,10 @@ impl Transpiler { args: &[Expr], ) -> Result { contract_pre_configuration!(); + // NESTPUSHLIT-1: an array literal pushed into a list of vec! literals. + if let Some(args) = self.inner_vec_push_args(object, method, args) { + return self.transpile_method_call_impl(object, method, &args); + } self.transpile_method_call_impl(object, method, args) } diff --git a/tests/g2b_transfix_6.rs b/tests/g2b_transfix_6.rs index ed2677401..7bbf85012 100644 --- a/tests/g2b_transfix_6.rs +++ b/tests/g2b_transfix_6.rs @@ -5,6 +5,9 @@ //! - PRINTPARAM-1: an untyped function or closure parameter whose emitted //! (or call-site inferred) type is `String` prints with `{}` through //! `println(s)`. Before, `p("hi")` and `f("hi")` printed `"hi"` with quotes. +//! - NESTPUSHLIT-1: pushing or inserting an array literal into a binding +//! whose inner literals became `vec![..]` (NESTARRVEC-1) emits the argument +//! as `vec![..]` too. Before, rustc refused it (E0308). use assert_cmd::Command; use std::path::Path; @@ -143,3 +146,48 @@ fn test_printparam_1_closure_list_param_keeps_debug() { ); check(&src, "[1, 2]\n", true); } + +// ------------------------------------------------------------ NESTPUSHLIT-1 + +#[test] +fn test_nestpushlit_1_push_literal_after_elem_growth() { + let src = main_with( + r#" let mut m = [[1]] + m[0].push(2) + m.push([3]) + println("{:?}", m)"#, + ); + check(&src, "[[1, 2], [3]]\n", true); +} + +#[test] +fn test_nestpushlit_1_insert_literal_after_elem_growth() { + let src = main_with( + r#" let mut m = [[1]] + m[0].push(2) + m.insert(0, [5, 6]) + println("{:?}", m)"#, + ); + check(&src, "[[5, 6], [1, 2]]\n", true); +} + +#[test] +fn test_nestpushlit_1_pushed_literal_grown_later() { + let src = main_with( + r#" let mut m = [[1]] + m.push([3]) + m[1].push(4) + println("{:?}", m)"#, + ); + check(&src, "[[1], [3, 4]]\n", true); +} + +#[test] +fn test_nestpushlit_1_no_elem_growth_keeps_arrays() { + let src = main_with( + r#" let mut m = [[1]] + m.push([3]) + println("{:?}", m)"#, + ); + check(&src, "[[1], [3]]\n", true); +} From 479052eb965da92e841787b4591949398b965ed3 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 13:16:43 +0200 Subject: [PATCH 101/106] RHLGA-1: ARRCLONE-1 arrays, tuples, strings and plain objects have clone - Arrays: clone, to_vec, to_owned (eval_array nullary dispatch) - Tuples: clone, to_owned (eval_method::eval_generic_method) - Strings: clone, to_owned (eval_string_methods zero-arg dispatch) - Plain objects: clone (eval_method_dispatch::eval_plain_object_method) - A clone is independent of its source; tests/g2b_runfix_6.rs (7 tests) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 + docs/roadmaps/roadmap.yaml | 4 +- src/runtime/eval_array.rs | 3 + src/runtime/eval_method.rs | 8 +- src/runtime/eval_method_dispatch.rs | 17 +++- src/runtime/eval_string_methods.rs | 3 +- tests/g2b_runfix_6.rs | 141 ++++++++++++++++++++++++++++ 7 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 tests/g2b_runfix_6.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a571db86c..eb85c865f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,9 @@ Behaviour changes a program can observe are marked **(behaviour)**. became `vec![..]` (NESTARRVEC-1) is emitted as `vec![..]` too, so `m[0].push(2); m.push([3])` compiles and prints `[[1, 2], [3]]` (NESTPUSHLIT-1). +- Arrays have `clone`, `to_vec` and `to_owned`; tuples and strings have + `clone` and `to_owned`; plain objects have `clone`. A clone is + independent: writes to it leave the original unchanged (ARRCLONE-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 4edf76d8d..2f62e26e5 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4794,11 +4794,11 @@ roadmap: github_issue: null item_type: task title: 'ARRCLONE-1: interpreter arrays have no clone method (Unknown array method: clone)' - status: planned + status: completed priority: medium assigned_to: null created: 2026-09-24T16:00:00Z - updated: 2026-09-24T16:00:00Z + updated: 2026-09-24T18:00:00Z spec: null acceptance_criteria: - 'Repro: let a = [1]; let b = a.clone(); println("{:?}", b)' diff --git a/src/runtime/eval_array.rs b/src/runtime/eval_array.rs index 20be0e51e..4d3bfc888 100644 --- a/src/runtime/eval_array.rs +++ b/src/runtime/eval_array.rs @@ -23,6 +23,9 @@ fn eval_array_nullary_method( "len" | "length" | "count" => eval_array_len(arr), // COUNTITER-1: `iter()` is the array itself, so `v.iter().count()` works "iter" => Ok(Value::Array(Arc::clone(arr))), + // ARRCLONE-1: a copy of the array; writes to either copy the shared + // storage first, so the copies stay independent + "clone" | "to_vec" | "to_owned" => Ok(Value::Array(Arc::clone(arr))), "first" => eval_array_first(arr), "last" => eval_array_last(arr), "is_empty" => eval_array_is_empty(arr), diff --git a/src/runtime/eval_method.rs b/src/runtime/eval_method.rs index ac69edf99..8a2ccdb79 100644 --- a/src/runtime/eval_method.rs +++ b/src/runtime/eval_method.rs @@ -139,7 +139,7 @@ pub fn eval_integer_method( /// Evaluate generic methods available on all types /// /// # Complexity -/// Cyclomatic complexity: 3 (within Toyota Way limits) +/// Cyclomatic complexity: 5 (within Toyota Way limits) pub fn eval_generic_method( receiver: &Value, method: &str, @@ -147,6 +147,12 @@ pub fn eval_generic_method( ) -> Result { if method == "to_string" && args_empty { Ok(Value::from_string(receiver.to_string())) + } else if matches!(method, "clone" | "to_owned") + && args_empty + && matches!(receiver, Value::Tuple(_)) + { + // ARRCLONE-1: tuples are immutable values, so the clone is the tuple + Ok(receiver.clone()) } else { Err(InterpreterError::RuntimeError(format!( "Method '{}' not found for type {}", diff --git a/src/runtime/eval_method_dispatch.rs b/src/runtime/eval_method_dispatch.rs index 3eb6aee56..33d38adfa 100644 --- a/src/runtime/eval_method_dispatch.rs +++ b/src/runtime/eval_method_dispatch.rs @@ -264,15 +264,26 @@ fn eval_plain_object_get( /// Returns `None` for any other method so the caller keeps its error path. /// /// # Complexity -/// Cyclomatic complexity: 6 +/// Cyclomatic complexity: 8 fn eval_plain_object_method( obj: &std::collections::HashMap, method: &str, arg_values: &[Value], ) -> Option> { - if method == "get" { - return Some(eval_plain_object_get(obj, arg_values)); + match method { + "get" => Some(eval_plain_object_get(obj, arg_values)), + // ARRCLONE-1: a copy of the map; field writes on it leave `obj` unchanged + "clone" if arg_values.is_empty() => Some(Ok(Value::Object(Arc::new(obj.clone())))), + _ => eval_plain_object_projection(obj, method, arg_values), } +} + +/// `keys`, `values`, `items`/`entries` of a plain object, sorted by key +fn eval_plain_object_projection( + obj: &std::collections::HashMap, + method: &str, + arg_values: &[Value], +) -> Option> { let project: fn(&String, &Value) -> Value = match method { "keys" => |k, _| Value::from_string(k.clone()), "values" => |_, v| v.clone(), diff --git a/src/runtime/eval_string_methods.rs b/src/runtime/eval_string_methods.rs index 9e132d345..7dd087fea 100644 --- a/src/runtime/eval_string_methods.rs +++ b/src/runtime/eval_string_methods.rs @@ -38,7 +38,8 @@ fn eval_zero_arg_string_method(s: &Arc, method: &str) -> Result Ok(Value::Integer(s.len() as i64)), "to_upper" | "to_uppercase" | "upper" => Ok(Value::from_string(s.to_uppercase())), "to_lower" | "to_lowercase" | "lower" => Ok(Value::from_string(s.to_lowercase())), - "to_string" => Ok(Value::from_string(s.to_string())), + // ARRCLONE-1: `clone`/`to_owned` copy the string, like `to_string` + "to_string" | "clone" | "to_owned" => Ok(Value::from_string(s.to_string())), "is_empty" => Ok(Value::Bool(s.is_empty())), "is_numeric" => Ok(Value::Bool(s.chars().all(char::is_numeric))), "is_alphabetic" => Ok(Value::Bool(s.chars().all(char::is_alphabetic))), diff --git a/tests/g2b_runfix_6.rs b/tests/g2b_runfix_6.rs new file mode 100644 index 000000000..661603075 --- /dev/null +++ b/tests/g2b_runfix_6.rs @@ -0,0 +1,141 @@ +//! RHLGA-1 G2b run fixes, part 6. +//! +//! - ARRCLONE-1: arrays had no `clone` method ("Unknown array method: +//! clone"), nor `to_vec`/`to_owned`; tuples, plain objects and strings had +//! no `clone` either, and strings and tuples no `to_owned`. A clone is independent of +//! its source: mutating it leaves the original unchanged. +//! +//! Where the transpiler can build the program, `ruchy run` is compared with +//! the binary `ruchy compile` builds from the same source. + +use assert_cmd::Command; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +fn write_source(dir: &Path, src: &str) -> PathBuf { + let file = dir.join("prog.ruchy"); + std::fs::write(&file, src).expect("write ruchy source"); + file +} + +fn ruchy() -> Command { + Command::cargo_bin("ruchy").expect("ruchy binary is built for tests") +} + +fn stdout_of(mut cmd: Command, what: &str) -> String { + let out = cmd.output().expect("spawn command"); + assert!( + out.status.success(), + "{what} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("utf8 stdout") +} + +fn run_cmd(dir: &Path, src: &str) -> Command { + let mut cmd = ruchy(); + cmd.arg("run") + .arg(write_source(dir, src)) + .timeout(Duration::from_secs(10)); + cmd +} + +fn compile_cmd(dir: &Path, src: &str) -> (Command, PathBuf) { + let bin = dir.join("prog_bin"); + let mut cmd = ruchy(); + cmd.arg("compile") + .arg(write_source(dir, src)) + .arg("-o") + .arg(&bin) + .timeout(Duration::from_secs(180)); + (cmd, bin) +} + +fn compiled_run(dir: &Path, src: &str) -> String { + let (cmd, bin) = compile_cmd(dir, src); + stdout_of(cmd, "ruchy compile"); + let mut run = Command::new(&bin); + run.timeout(Duration::from_secs(10)); + stdout_of(run, "compiled binary") +} + +/// The compiled binary prints `expected`, and `ruchy run` prints it too. +fn assert_run_matches_compiled(src: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!(compiled_run(dir.path(), src), expected, "compiled binary"); + assert_eq!( + stdout_of(run_cmd(dir.path(), src), "ruchy run"), + expected, + "ruchy run" + ); +} + +/// `ruchy run` prints `expected`: the output Rust semantics give. Used where +/// the transpiler cannot build the program yet. +fn assert_run_prints(src: &str, expected: &str) { + let dir = tempfile::tempdir().expect("tempdir"); + assert_eq!( + stdout_of(run_cmd(dir.path(), src), "ruchy run"), + expected, + "ruchy run" + ); +} + +// ------------------------------------------------------------- ARRCLONE-1 + +#[test] +fn test_arrclone_1_array_clone_repro() { + assert_run_matches_compiled( + "let a = [1]\nlet b = a.clone()\nprintln(\"{:?}\", b)\n", + "[1]\n", + ); +} + +// The transpiler builds `[1, 2]` as a fixed array, which has no `push`. +#[test] +fn test_arrclone_1_array_clone_is_independent() { + assert_run_prints( + "let a = [1, 2]\nlet mut b = a.clone()\nb.push(3)\nb[0] = 9\nprintln(\"{:?}\", a)\nprintln(\"{:?}\", b)\n", + "[1, 2]\n[9, 2, 3]\n", + ); +} + +#[test] +fn test_arrclone_1_nested_array_clone_is_independent() { + assert_run_prints( + "let a = [[1], [2]]\nlet mut b = a.clone()\nb[0] = [7]\nprintln(\"{:?}\", a)\nprintln(\"{:?}\", b)\n", + "[[1], [2]]\n[[7], [2]]\n", + ); +} + +#[test] +fn test_arrclone_1_array_to_vec_and_to_owned() { + assert_run_matches_compiled( + "let a = [1, 2]\nprintln(\"{:?}\", a.to_vec())\nprintln(\"{:?}\", a.to_owned())\n", + "[1, 2]\n[1, 2]\n", + ); +} + +#[test] +fn test_arrclone_1_tuple_clone() { + assert_run_matches_compiled( + "let t = (1, \"x\")\nlet u = t.clone()\nprintln(\"{:?}\", u)\nprintln(\"{:?}\", t.to_owned())\n", + "(1, \"x\")\n(1, \"x\")\n", + ); +} + +#[test] +fn test_arrclone_1_string_clone_and_to_owned() { + assert_run_matches_compiled( + "let s = \"hi\"\nlet r = s.clone()\nprintln(\"{}\", r)\nprintln(\"{}\", s.to_owned())\n", + "hi\nhi\n", + ); +} + +#[test] +fn test_arrclone_1_object_clone_is_independent() { + assert_run_prints( + "let o = {x: 1}\nlet mut p = o.clone()\np.x = 5\nprintln(\"{:?}\", o.x)\nprintln(\"{:?}\", p.x)\n", + "1\n5\n", + ); +} From 2c1296fd8f2d171462d356b615bed9bda0682297 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 13:46:43 +0200 Subject: [PATCH 102/106] RHLGA-1: LONELIT-1 run prints a lone println/print string literal verbatim, as compiled code does Found by the G2b final quorum (second sonnet seat). format(lit) stays a template. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 +++ docs/roadmaps/roadmap.yaml | 19 ++++++++++++++ src/runtime/interpreter_format_calls.rs | 7 ++++-- tests/g2b_runfix_6.rs | 33 +++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb85c865f..2d9a2f2e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -183,6 +183,10 @@ Behaviour changes a program can observe are marked **(behaviour)**. - Arrays have `clone`, `to_vec` and `to_owned`; tuples and strings have `clone` and `to_owned`; plain objects have `clone`. A clone is independent: writes to it leave the original unchanged (ARRCLONE-1). +- `ruchy run` prints a lone string literal passed to `println`/`print` + verbatim, as the compiled binary does: `println("{}")` prints `{}`, not a + format error, and `println("x{{")` prints `x{{`. `format("x{{")` is still + a template and returns `x{` (LONELIT-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 2f62e26e5..190dc755a 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4828,3 +4828,22 @@ roadmap: labels: - kind:code - transpiler +- id: LONELIT-1 + github_issue: null + item_type: task + title: 'LONELIT-1: interpreter println/print parse a lone string literal as a format template; the compiled binary prints it verbatim' + status: completed + priority: high + assigned_to: null + created: 2026-09-24T19:00:00Z + updated: 2026-09-24T19:30:00Z + spec: null + acceptance_criteria: + - 'Repro: println("{}") is a runtime format error under ruchy run; the compiled binary prints {} (found by the G2b final quorum, second sonnet seat)' + - 'Acceptance: tests/g2b_runfix_6.rs test_lonelit_1_* compare ruchy run with the compiled binary' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime diff --git a/src/runtime/interpreter_format_calls.rs b/src/runtime/interpreter_format_calls.rs index dbe505f1b..cee57aa76 100644 --- a/src/runtime/interpreter_format_calls.rs +++ b/src/runtime/interpreter_format_calls.rs @@ -143,8 +143,11 @@ impl Interpreter { let (positional, named) = self.eval_format_args(args)?; // FMTTEMPLATE-1: only a string literal is a format template, as in // the transpiler; `println(s, 1)` prints its arguments joined by - // spaces. - let template = args.first().is_some_and(is_string_literal); + // spaces. LONELIT-1: a lone literal is a template only for + // `format` (`format!(lit)`); println/print print it verbatim, as the + // transpiler escapes its braces. + let template = args.first().is_some_and(is_string_literal) + && (args.len() > 1 || matches!(sink, FormatSink::Return)); let resolve = |name: &str| { named .iter() diff --git a/tests/g2b_runfix_6.rs b/tests/g2b_runfix_6.rs index 661603075..62f1207ac 100644 --- a/tests/g2b_runfix_6.rs +++ b/tests/g2b_runfix_6.rs @@ -139,3 +139,36 @@ fn test_arrclone_1_object_clone_is_independent() { "1\n5\n", ); } + +// ------------------------------------------------------------- LONELIT-1 + +// A lone string literal passed to println/print is printed verbatim by the +// compiled binary (the transpiler escapes its braces); `ruchy run` agrees. +#[test] +fn test_lonelit_1_lone_println_literal_is_verbatim() { + assert_run_matches_compiled( + "println(\"{}\")\nprintln(\"x{{\")\nprintln(\"a}\")\n", + "{}\nx{{\na}\n", + ); +} + +#[test] +fn test_lonelit_1_lone_print_literal_is_verbatim() { + assert_run_matches_compiled("print(\"{{}}\")\nprintln(\"\")\n", "{{}}\n"); +} + +#[test] +fn test_lonelit_1_lone_literal_does_not_capture_names() { + assert_run_matches_compiled("let x = 1\nprintln(\"{x}\")\n", "{x}\n"); +} + +// `format` with a lone literal is `format!(lit)`: still a template. +#[test] +fn test_lonelit_1_lone_format_literal_is_template() { + assert_run_matches_compiled("let s = format(\"x{{\")\nprintln(s)\n", "x{\n"); +} + +#[test] +fn test_lonelit_1_literal_with_args_is_template() { + assert_run_matches_compiled("println(\"{}!\", 1)\nprintln(\"{{{}}}\", 2)\n", "1!\n{2}\n"); +} From 33b55af76c38180ff488e15d2a92ca4aa6ce716d Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 13:58:43 +0200 Subject: [PATCH 103/106] RHLGA-1: STRRECV-1 replace/repeat on an untracked receiver and loop variables over string items print as text Found by the G2b round-2 quorum (sonnet seat). Trailing for/while/+= in main are unit; strings gain split_whitespace in the interpreter. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 7 ++ docs/roadmaps/roadmap.yaml | 19 ++++ src/backend/transpiler/control_flow.rs | 2 +- src/backend/transpiler/function_transpiler.rs | 8 ++ src/backend/transpiler/print_helpers.rs | 82 +++++++++++++-- src/runtime/eval_string_methods.rs | 6 ++ tests/g2b_transfix_6.rs | 99 +++++++++++++++++++ 7 files changed, 216 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9a2f2e1..d2e9ec96c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -187,6 +187,13 @@ Behaviour changes a program can observe are marked **(behaviour)**. verbatim, as the compiled binary does: `println("{}")` prints `{}`, not a format error, and `println("x{{")` prints `x{{`. `format("x{{")` is still a template and returns `x{` (LONELIT-1). +- Compiled `println(x.replace(..))` and `println(x.repeat(n))` print as text + unless `x` is a list literal or a binding to one; a loop variable, field or + call receiver no longer prints quoted. A plain loop variable over + `lines()`, `split_whitespace()`, `split()` or a list of string + literals prints as text. A `for`/`while` loop or `+=` as main's last + statement is not printed as `()`. Strings have `split_whitespace()` under + `ruchy run` (STRRECV-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 190dc755a..f7f02fa0f 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4847,3 +4847,22 @@ roadmap: labels: - kind:code - runtime +- id: STRRECV-1 + github_issue: null + item_type: task + title: 'STRRECV-1: compiled println of replace/repeat on an untracked string receiver prints quoted ({:?}); ruchy run prints text' + status: completed + priority: high + assigned_to: null + created: 2026-09-24T20:00:00Z + updated: 2026-09-24T20:30:00Z + spec: null + acceptance_criteria: + - 'Repro: for line in text.lines() { println(line.replace("a","b")) } prints "bb" quoted when compiled (found by G2b round-2 quorum, sonnet seat); a trailing for loop in main printed ()' + - 'Acceptance: tests/g2b_transfix_6.rs test_strrecv_1_* compare compiled stdout with ruchy run' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - transpiler diff --git a/src/backend/transpiler/control_flow.rs b/src/backend/transpiler/control_flow.rs index 75778824a..27290c465 100644 --- a/src/backend/transpiler/control_flow.rs +++ b/src/backend/transpiler/control_flow.rs @@ -74,7 +74,7 @@ impl Transpiler { let loop_pattern = pattern .cloned() .unwrap_or_else(|| Pattern::Identifier(var.to_string())); - let body_tokens = self.with_pattern_scope(&loop_pattern, || self.transpile_expr(body)); + let body_tokens = self.with_for_scope(&loop_pattern, iter, || self.transpile_expr(body)); self.in_loop_context.set(was_in_loop); let body_tokens = body_tokens?; diff --git a/src/backend/transpiler/function_transpiler.rs b/src/backend/transpiler/function_transpiler.rs index 6aa16d70d..9eefd87f7 100644 --- a/src/backend/transpiler/function_transpiler.rs +++ b/src/backend/transpiler/function_transpiler.rs @@ -171,6 +171,14 @@ impl Transpiler { ExprKind::MacroInvocation { name, .. } if Self::is_print_name(name) ) || matches!(&expr.kind, ExprKind::Assign { .. }) + // STRRECV-1: loops without a break value and compound assignment + || matches!( + &expr.kind, + ExprKind::For { .. } + | ExprKind::While { .. } + | ExprKind::WhileLet { .. } + | ExprKind::CompoundAssign { .. } + ) || matches!(&expr.kind, ExprKind::Return { value: None }) || matches!(&expr.kind, ExprKind::Return { value: Some(_) }) // Return with value is still unit for main } diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index 2e49ca56e..f0eb74b3d 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -246,6 +246,10 @@ impl Transpiler { /// PRINTSTR-1: the type tag `variable_types` holds for a string binding. const STRING_VAR_TYPE: &str = "String"; +/// STRRECV-1: the type tag `variable_types` holds for a binding to a list +/// literal, whose `repeat` is a `Vec`. +const LIST_VAR_TYPE: &str = "Vec"; + /// PRINTSTR-1: methods whose result prints as text on every receiver they /// exist on (`to_string` on anything, the case/trim methods on `str`/`char`). const STRING_RESULT_METHODS: &[&str] = &[ @@ -257,8 +261,10 @@ const STRING_RESULT_METHODS: &[&str] = &[ "trim_end", ]; -/// PRINTSTRSCOPE-1: methods whose result is a `String` only on a string -/// receiver (`[1].repeat(2)` is a `Vec`, `Option::replace` an `Option`). +/// PRINTSTRSCOPE-1: methods whose result is a `String` except on a list +/// receiver (`[1].repeat(2)` is a `Vec`); STRRECV-1: an untracked receiver +/// (a loop variable, a field, a call) counts as a string, as `ruchy run` +/// prints it. const STRING_RECEIVER_METHODS: &[&str] = &["replace", "repeat"]; impl Transpiler { @@ -301,6 +307,11 @@ impl Transpiler { pub(crate) fn track_string_binding(&self, name: &str, value: &Expr, ty: Option<&Type>) { let is_string = ty.map_or_else(|| self.is_display_string(value), is_string_annotation); self.set_string_var(name, is_string); + if ty.is_none() && self.is_known_list(value) { + self.variable_types + .borrow_mut() + .insert(name.to_string(), LIST_VAR_TYPE.to_string()); + } self.track_typed_dict_binding(name, value); } @@ -316,10 +327,25 @@ impl Transpiler { } /// PRINTSTRSCOPE-1: `receiver.method(..)` is a string: a text-printing - /// method, or `replace`/`repeat` on a string receiver. (complexity: 3) + /// method, or `replace`/`repeat` on a receiver that is not a list + /// (STRRECV-1). (complexity: 3) fn is_string_method_result(&self, receiver: &Expr, method: &str) -> bool { STRING_RESULT_METHODS.contains(&method) - || (STRING_RECEIVER_METHODS.contains(&method) && self.is_display_string(receiver)) + || (STRING_RECEIVER_METHODS.contains(&method) && !self.is_known_list(receiver)) + } + + /// STRRECV-1: `expr` is a list literal or a binding recorded as one. + /// (complexity: 3) + fn is_known_list(&self, expr: &Expr) -> bool { + match &expr.kind { + ExprKind::List(_) | ExprKind::ArrayInit { .. } => true, + ExprKind::Identifier(name) => self + .variable_types + .borrow() + .get(name) + .is_some_and(|t| t == LIST_VAR_TYPE), + _ => false, + } } /// PRINTSTRSCOPE-1: run `f` while each `(name, is_string)` binder @@ -364,6 +390,23 @@ impl Transpiler { self.with_string_scope(&binders, f) } + /// STRRECV-1: run `f` in the scope of a for loop's binders; a plain loop + /// variable over string items (see [`is_string_items`]) is a string. + /// (complexity: 2) + pub(crate) fn with_for_scope( + &self, + pattern: &Pattern, + iter: &Expr, + f: impl FnOnce() -> T, + ) -> T { + match pattern { + Pattern::Identifier(name) if is_string_items(iter) => { + self.with_string_scope(&[(name.clone(), true)], f) + } + _ => self.with_pattern_scope(pattern, f), + } + } + /// PRINTSTRSCOPE-1: run `f` in the scope of closure parameters; a /// `String`/`&str`-annotated parameter is a string. (complexity: 1) pub(crate) fn with_param_scope(&self, params: &[Param], f: impl FnOnce() -> T) -> T { @@ -417,12 +460,16 @@ impl Transpiler { } } - /// PRINTSTR-1: set or clear the string record of `name`. (complexity: 3) + /// PRINTSTR-1: set or clear the string record of `name`; STRRECV-1: a + /// rebinding also clears its list record. (complexity: 3) fn set_string_var(&self, name: &str, is_string: bool) { let mut types = self.variable_types.borrow_mut(); if is_string { types.insert(name.to_string(), STRING_VAR_TYPE.to_string()); - } else if types.get(name).is_some_and(|t| t == STRING_VAR_TYPE) { + } else if types + .get(name) + .is_some_and(|t| t == STRING_VAR_TYPE || t == LIST_VAR_TYPE) + { types.remove(name); } } @@ -465,6 +512,29 @@ impl Transpiler { } } +/// STRRECV-1: iterating `iter` yields strings: `lines()`, +/// `split_whitespace()`, `split()`, or a list of +/// string literals. (complexity: 4) +fn is_string_items(iter: &Expr) -> bool { + match &iter.kind { + ExprKind::MethodCall { method, args, .. } => match method.as_str() { + "lines" | "split_whitespace" => args.is_empty(), + "split" => matches!( + args.first().map(|a| &a.kind), + Some(ExprKind::Literal(Literal::String(_) | Literal::Char(_))) + ), + _ => false, + }, + ExprKind::List(items) => { + !items.is_empty() + && items + .iter() + .all(|i| matches!(i.kind, ExprKind::Literal(Literal::String(_)))) + } + _ => false, + } +} + /// PRINTPARAM-1: the emitted type text `ty` is `String`, `&str` or /// `&'a str`. (complexity: 3) fn is_string_type_text(ty: &str) -> bool { diff --git a/src/runtime/eval_string_methods.rs b/src/runtime/eval_string_methods.rs index 7dd087fea..e098750c0 100644 --- a/src/runtime/eval_string_methods.rs +++ b/src/runtime/eval_string_methods.rs @@ -49,6 +49,12 @@ fn eval_zero_arg_string_method(s: &Arc, method: &str) -> Result Ok(Value::from_string(s.trim_end().to_string())), "chars" => eval_string_chars(s), "lines" => eval_string_lines(s), + // STRRECV-1: whitespace-separated words, as `str::split_whitespace` + "split_whitespace" => Ok(Value::from_array( + s.split_whitespace() + .map(|w| Value::from_string(w.to_string())) + .collect(), + )), "parse" | "to_int" | "to_integer" => eval_string_parse(s), "timestamp" => eval_string_timestamp(s), "to_rfc3339" => Ok(Value::from_string(s.to_string())), diff --git a/tests/g2b_transfix_6.rs b/tests/g2b_transfix_6.rs index 7bbf85012..d5c102eaf 100644 --- a/tests/g2b_transfix_6.rs +++ b/tests/g2b_transfix_6.rs @@ -191,3 +191,102 @@ fn test_nestpushlit_1_no_elem_growth_keeps_arrays() { ); check(&src, "[[1], [3]]\n", true); } + +// ------------------------------------------------------------ STRRECV-1 + +// `replace`/`repeat` on a receiver the transpiler cannot prove is a string +// (a for-loop variable, a struct field, `String::from(..)`) print as text. +#[test] +fn test_strrecv_1_for_variable_replace_prints_text() { + let src = main_with( + r#" let text = "ab\ncd" + for line in text.lines() { + println(line.replace("a", "b")) + }"#, + ); + check(&src, "bb\ncd\n", true); +} + +#[test] +fn test_strrecv_1_field_replace_prints_text() { + let src = format!( + "struct P {{ name: String }}\n{}", + main_with( + r#" let p = P { name: String::from("xa") } + println(p.name.replace("x", "y"))"# + ) + ); + check(&src, "ya\n", true); +} + +#[test] +fn test_strrecv_1_call_receiver_repeat_prints_text() { + let src = main_with( + r#" let s = String::from("q") + println(s.repeat(2)) + println(String::from("z").repeat(3))"#, + ); + check(&src, "qq\nzzz\n", true); +} + +// A list receiver still prints its `repeat` with `{:?}` (PRINTSTRSCOPE-1). +#[test] +fn test_strrecv_1_list_repeat_stays_debug() { + let src = main_with( + r#" let v = [1] + println(v.repeat(2)) + println([2].repeat(2))"#, + ); + check(&src, "[1, 1]\n[2, 2]\n", true); +} + +// A string rebinding of a list name prints its `repeat` as text again. +#[test] +fn test_strrecv_1_list_name_rebound_to_string() { + let src = main_with( + r#" let v = [1] + println(v.len()) + let v = String::from("ab") + println(v.repeat(2))"#, + ); + check(&src, "1\nabab\n", true); +} + +// A loop variable over string items prints as text. +#[test] +fn test_strrecv_1_loop_variable_over_string_items() { + let src = main_with( + r#" let text = "a b\nc" + for line in text.lines() { + println(line) + } + for w in text.split_whitespace() { + println(w) + } + for p in "x,y".split(",") { + println(p) + } + for s in ["m", "n"] { + println(s) + }"#, + ); + check(&src, "a b\nc\na\nb\nc\nx\ny\nm\nn\n", true); +} + +// A for loop, a while loop or a compound assignment as main's last +// statement is unit: the compiled binary prints nothing for it. +#[test] +fn test_strrecv_1_trailing_loops_are_unit() { + let src = main_with( + r#" let mut i = 0 + while i < 2 { + i += 1 + } + println(i) + for k in [1] { + println(k) + }"#, + ); + check(&src, "2\n1\n", true); + check(&main_with(" let mut j = 1\n j += 1"), "", true); +} From b1f3a01fa87a2554261fdc9ffc42c5e644775c4a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 14:07:17 +0200 Subject: [PATCH 104/106] RHLGA-1: STRRECV-1 replace is a string by arity; repeat stays Debug on list-typed params and bindings Found by the G2b round-3 confirmation seat (sonnet): an untracked Vec/Option receiver got {} and failed rustc. Vec records are cleared per function. Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 10 +++-- src/backend/transpiler/print_helpers.rs | 58 +++++++++++++++++-------- tests/g2b_transfix_6.rs | 18 ++++++++ 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e9ec96c..460971c34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -187,9 +187,13 @@ Behaviour changes a program can observe are marked **(behaviour)**. verbatim, as the compiled binary does: `println("{}")` prints `{}`, not a format error, and `println("x{{")` prints `x{{`. `format("x{{")` is still a template and returns `x{` (LONELIT-1). -- Compiled `println(x.replace(..))` and `println(x.repeat(n))` print as text - unless `x` is a list literal or a binding to one; a loop variable, field or - call receiver no longer prints quoted. A plain loop variable over +- Compiled `println(x.replace(a, b))` prints as text for any receiver (the + two-argument `replace` is `str::replace`; `Option::replace` takes one), and + `println(x.repeat(n))` prints as text unless `x` is known to be a list: a + list literal, a binding to one, or a binding or parameter annotated + `Vec`/`[T]`. A loop variable, field or call receiver no longer prints + quoted; a list receiver the transpiler cannot see (an untyped parameter + bound to a list) now needs an annotation. A plain loop variable over `lines()`, `split_whitespace()`, `split()` or a list of string literals prints as text. A `for`/`while` loop or `+=` as main's last statement is not printed as `()`. Strings have `split_whitespace()` under diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index f0eb74b3d..f4594c61f 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -261,12 +261,6 @@ const STRING_RESULT_METHODS: &[&str] = &[ "trim_end", ]; -/// PRINTSTRSCOPE-1: methods whose result is a `String` except on a list -/// receiver (`[1].repeat(2)` is a `Vec`); STRRECV-1: an untracked receiver -/// (a loop variable, a field, a call) counts as a string, as `ruchy run` -/// prints it. -const STRING_RECEIVER_METHODS: &[&str] = &["replace", "repeat"]; - impl Transpiler { /// PRINTSTR-1: `{}` for an expression known to be a string, `{:?}` for /// anything else (a number prints the same either way). (complexity: 2) @@ -289,8 +283,10 @@ impl Transpiler { } ExprKind::Call { func, args } => self.is_format_fn_call(func, args), ExprKind::MethodCall { - receiver, method, .. - } => self.is_string_method_result(receiver, method), + receiver, + method, + args, + } => self.is_string_method_result(receiver, method, args), ExprKind::Identifier(name) => self .variable_types .borrow() @@ -307,11 +303,10 @@ impl Transpiler { pub(crate) fn track_string_binding(&self, name: &str, value: &Expr, ty: Option<&Type>) { let is_string = ty.map_or_else(|| self.is_display_string(value), is_string_annotation); self.set_string_var(name, is_string); - if ty.is_none() && self.is_known_list(value) { - self.variable_types - .borrow_mut() - .insert(name.to_string(), LIST_VAR_TYPE.to_string()); - } + self.set_list_var( + name, + ty.map_or_else(|| self.is_known_list(value), is_list_annotation), + ); self.track_typed_dict_binding(name, value); } @@ -320,18 +315,32 @@ impl Transpiler { pub(crate) fn track_string_params(&self, params: &[Param]) { self.variable_types .borrow_mut() - .retain(|_, t| t != STRING_VAR_TYPE); + .retain(|_, t| t != STRING_VAR_TYPE && t != LIST_VAR_TYPE); for param in params { self.set_string_var(¶m.name(), is_string_annotation(¶m.ty)); + self.set_list_var(¶m.name(), is_list_annotation(¶m.ty)); } } /// PRINTSTRSCOPE-1: `receiver.method(..)` is a string: a text-printing - /// method, or `replace`/`repeat` on a receiver that is not a list - /// (STRRECV-1). (complexity: 3) - fn is_string_method_result(&self, receiver: &Expr, method: &str) -> bool { - STRING_RESULT_METHODS.contains(&method) - || (STRING_RECEIVER_METHODS.contains(&method) && !self.is_known_list(receiver)) + /// method; STRRECV-1: `replace(from, to)` (`Option::replace` takes one + /// argument), or `repeat` on a receiver not known to be a list. + /// (complexity: 4) + fn is_string_method_result(&self, receiver: &Expr, method: &str, args: &[Expr]) -> bool { + match method { + "replace" => args.len() == 2, + "repeat" => !self.is_known_list(receiver), + _ => STRING_RESULT_METHODS.contains(&method), + } + } + + /// STRRECV-1: record `name` as a list binding (or leave it). (complexity: 2) + fn set_list_var(&self, name: &str, is_list: bool) { + if is_list { + self.variable_types + .borrow_mut() + .insert(name.to_string(), LIST_VAR_TYPE.to_string()); + } } /// STRRECV-1: `expr` is a list literal or a binding recorded as one. @@ -512,6 +521,17 @@ impl Transpiler { } } +/// STRRECV-1: `ty` is a list type: `[T]`, `[T; N]`, `Vec`, or a +/// reference to one. (complexity: 4) +fn is_list_annotation(ty: &Type) -> bool { + match &ty.kind { + TypeKind::List(_) | TypeKind::Array { .. } => true, + TypeKind::Generic { base, .. } => base == "Vec", + TypeKind::Reference { inner, .. } => is_list_annotation(inner), + _ => false, + } +} + /// STRRECV-1: iterating `iter` yields strings: `lines()`, /// `split_whitespace()`, `split()`, or a list of /// string literals. (complexity: 4) diff --git a/tests/g2b_transfix_6.rs b/tests/g2b_transfix_6.rs index d5c102eaf..61049dd2c 100644 --- a/tests/g2b_transfix_6.rs +++ b/tests/g2b_transfix_6.rs @@ -290,3 +290,21 @@ fn test_strrecv_1_trailing_loops_are_unit() { check(&src, "2\n1\n", true); check(&main_with(" let mut j = 1\n j += 1"), "", true); } + +// A list-typed parameter or annotated binding keeps `repeat` on `{:?}` +// (a `{}` would not build: `Vec` has no Display). +#[test] +fn test_strrecv_1_list_typed_param_repeat_stays_debug() { + let src = format!( + "fun f(v: Vec) {{\n println(v.repeat(2))\n}}\n{}", + main_with(" let w: Vec = vec![3]\n println(w.repeat(2))\n f(vec![1])") + ); + check(&src, "[3, 3]\n[1, 1]\n", true); +} + +// `Option::replace` takes one argument and is not a string. +#[test] +fn test_strrecv_1_option_replace_stays_debug() { + let src = main_with(" let mut o = Some(1)\n println(o.replace(2))\n println(o)"); + check(&src, "Some(1)\nSome(2)\n", false); +} From 93efa28f6901aae49e1949af9aaab7289f79ab37 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 14:08:13 +0200 Subject: [PATCH 105/106] RHLGA-1: file DEADDISPATCH-1 (unreachable duplicate method dispatch) Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index f7f02fa0f..8aa755904 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -4866,3 +4866,23 @@ roadmap: labels: - kind:code - transpiler +- id: DEADDISPATCH-1 + github_issue: null + item_type: task + title: 'DEADDISPATCH-1: duplicate string/generic method dispatch (src/runtime/eval_string.rs, eval_generic_method in eval_method_dispatch.rs) is unreachable and drifts from the live copies' + status: planned + priority: medium + assigned_to: null + created: 2026-09-24T21:00:00Z + updated: 2026-09-24T21:00:00Z + spec: null + acceptance_criteria: + - 'Found by the ARRCLONE-1 worker and seen again in STRRECV-1: split_whitespace was added to eval_string_methods.rs only' + - 'Acceptance: one dispatch per receiver kind; pmat query --literal "Unknown zero-argument string method" returns one site' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + - runtime + - milestone:backlog From fb58f641fcb6e1bd13520aa85d408688c4a8df0d Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 24 Sep 2026 14:20:14 +0200 Subject: [PATCH 106/106] RHLGA-1: STRRECV-1 vec![..] and vec![x; n] are known lists; run evaluates vec![x; n] Found by the G2b round-4 quorum (sonnet and agy seats). Pmat-Ticket: RHLGA-1 Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 7 ++++--- src/backend/transpiler/print_helpers.rs | 9 ++++++--- src/runtime/eval_expr.rs | 1 + src/runtime/interpreter.rs | 2 ++ tests/g2b_transfix_6.rs | 9 +++++++++ 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 460971c34..6adbd1400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -190,14 +190,15 @@ Behaviour changes a program can observe are marked **(behaviour)**. - Compiled `println(x.replace(a, b))` prints as text for any receiver (the two-argument `replace` is `str::replace`; `Option::replace` takes one), and `println(x.repeat(n))` prints as text unless `x` is known to be a list: a - list literal, a binding to one, or a binding or parameter annotated + list literal (`[..]`, `[x; n]`, `vec![..]`, `vec![x; n]`), a binding to + one, or a binding or parameter annotated `Vec`/`[T]`. A loop variable, field or call receiver no longer prints quoted; a list receiver the transpiler cannot see (an untyped parameter bound to a list) now needs an annotation. A plain loop variable over `lines()`, `split_whitespace()`, `split()` or a list of string literals prints as text. A `for`/`while` loop or `+=` as main's last - statement is not printed as `()`. Strings have `split_whitespace()` under - `ruchy run` (STRRECV-1). + statement is not printed as `()`. Strings have `split_whitespace()` and + `vec![x; n]` evaluates under `ruchy run` (STRRECV-1). - Mutation detection walks every expression kind, including macro arguments. - Tests: - they spawn the cargo-built binary; diff --git a/src/backend/transpiler/print_helpers.rs b/src/backend/transpiler/print_helpers.rs index f4594c61f..e9611ef49 100644 --- a/src/backend/transpiler/print_helpers.rs +++ b/src/backend/transpiler/print_helpers.rs @@ -343,11 +343,14 @@ impl Transpiler { } } - /// STRRECV-1: `expr` is a list literal or a binding recorded as one. - /// (complexity: 3) + /// STRRECV-1: `expr` is a list literal (`[..]`, `[x; n]`, `vec![..]`, + /// `vec![x; n]`) or a binding recorded as one. (complexity: 4) fn is_known_list(&self, expr: &Expr) -> bool { match &expr.kind { - ExprKind::List(_) | ExprKind::ArrayInit { .. } => true, + ExprKind::List(_) | ExprKind::ArrayInit { .. } | ExprKind::VecRepeat { .. } => true, + ExprKind::Macro { name, .. } | ExprKind::MacroInvocation { name, .. } => { + name.trim_end_matches('!') == "vec" + } ExprKind::Identifier(name) => self .variable_types .borrow() diff --git a/src/runtime/eval_expr.rs b/src/runtime/eval_expr.rs index 0582814bf..760945406 100644 --- a/src/runtime/eval_expr.rs +++ b/src/runtime/eval_expr.rs @@ -98,6 +98,7 @@ pub fn is_data_structure_expr(expr_kind: &ExprKind) -> bool { | ExprKind::Tuple(_) | ExprKind::Range { .. } | ExprKind::ArrayInit { .. } + | ExprKind::VecRepeat { .. } | ExprKind::DataFrame { .. } ) } diff --git a/src/runtime/interpreter.rs b/src/runtime/interpreter.rs index 09aaaf012..db1dbded0 100644 --- a/src/runtime/interpreter.rs +++ b/src/runtime/interpreter.rs @@ -544,6 +544,8 @@ impl Interpreter { inclusive, } => self.eval_range_expr(start, end, *inclusive), ExprKind::ArrayInit { value, size } => self.eval_array_init_expr(value, size), + // STRRECV-1: `vec![x; n]` builds the same list as `[x; n]` + ExprKind::VecRepeat { value, count } => self.eval_array_init_expr(value, count), ExprKind::DataFrame { columns } => self.eval_dataframe_literal(columns), _ => unreachable!("Non-data-structure expression passed to eval_data_structure_expr"), } diff --git a/tests/g2b_transfix_6.rs b/tests/g2b_transfix_6.rs index 61049dd2c..008d53368 100644 --- a/tests/g2b_transfix_6.rs +++ b/tests/g2b_transfix_6.rs @@ -308,3 +308,12 @@ fn test_strrecv_1_option_replace_stays_debug() { let src = main_with(" let mut o = Some(1)\n println(o.replace(2))\n println(o)"); check(&src, "Some(1)\nSome(2)\n", false); } + +// `vec![..]` and `vec![x; n]` receivers and bindings are lists too. +#[test] +fn test_strrecv_1_vec_macro_repeat_stays_debug() { + let src = main_with( + " let v = vec![1]\n println(v.repeat(2))\n let z = vec![0; 2]\n println(z.repeat(2))\n println(vec![5].repeat(2))", + ); + check(&src, "[1, 1]\n[0, 0, 0, 0]\n[5, 5]\n", true); +}