From d1f3153057cb127f07e826aaad3b5d1c8cbb6bec Mon Sep 17 00:00:00 2001 From: onatozmenn Date: Fri, 14 Aug 2026 15:55:39 +0300 Subject: [PATCH 1/2] Correct the minimum Rust version --- .github/workflows/ci.yml | 15 +++++++++++++++ CHANGELOG.md | 5 ++++- Cargo.toml | 2 +- README.md | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 942a3dc..da671db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,21 @@ jobs: - name: cargo test --doc run: cargo test --doc --workspace + msrv: + name: minimum supported Rust + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.88.0 + + - uses: Swatinem/rust-cache@v2 + + - name: cargo check + run: cargo check --workspace --locked + mutants: name: mutants on the diff, shard ${{ matrix.shard }} runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b5d48..3849885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,10 @@ release notes. ### Tools -- None yet. +- The declared minimum Rust toolchain is now 1.88, matching the let-chain + syntax already used by the parser and resolver. Rust 1.87 and older failed + before `deed-lang` could compile. CI now builds the workspace with 1.88 so + the declaration cannot silently fall behind the source again. ### Measurements diff --git a/Cargo.toml b/Cargo.toml index 94c0853..960977f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ version = "0.2.11" edition = "2024" license = "Apache-2.0" repository = "https://github.com/deed-lang/deed" -rust-version = "1.85" +rust-version = "1.88" # Publishing needs a version next to every path, or `cargo publish` refuses, # and a version repeated in twenty manifests is nineteen places to forget. So diff --git a/README.md b/README.md index 025046f..12a6849 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ $ ./deed-v0.2.11-x86_64-unknown-linux-gnu/deed --version deed 0.2.11 ``` -With Rust 1.85 or newer instead, from crates.io or from a clone: +With Rust 1.88 or newer instead, from crates.io or from a clone: ``` $ cargo install deed-lang From 78723b69e432e7cce95b7e66a94f78d38e3617d4 Mon Sep 17 00:00:00 2001 From: onatozmenn Date: Fri, 14 Aug 2026 16:08:43 +0300 Subject: [PATCH 2/2] Satisfy Rust 1.88 Clippy --- crates/deed-cli/src/main.rs | 26 +++++++------- crates/deed-codegen/src/compile.rs | 2 +- crates/deed-driver/tests/explain_pages.rs | 28 +++++++-------- crates/deed-driver/tests/publishing.rs | 18 +++++----- crates/deed-interp/src/interp.rs | 6 ++-- crates/deed-interp/src/property.rs | 4 +-- crates/deed-lsp/src/server.rs | 37 +++++++++---------- crates/deed-mir/src/lower.rs | 8 ++--- crates/deed-parser/src/parser.rs | 30 ++++++++-------- crates/deed-typeck/src/check.rs | 44 +++++++++++------------ 10 files changed, 96 insertions(+), 107 deletions(-) diff --git a/crates/deed-cli/src/main.rs b/crates/deed-cli/src/main.rs index d81453e..27d10da 100644 --- a/crates/deed-cli/src/main.rs +++ b/crates/deed-cli/src/main.rs @@ -2483,13 +2483,12 @@ fn resolve_imports( let mut seed_texts: Vec = Vec::new(); for path in files.iter() { let text = std::fs::read_to_string(path).unwrap_or_default(); - if let Some((module, _)) = deed_driver::imports_of(&text) { - if let Some(root) = root_of(path, &module) { - if known_roots.insert(root.clone()) { - read_manifest(&root, &mut component_roots, &mut fetched, manifests); - roots.push(root); - } - } + if let Some((module, _)) = deed_driver::imports_of(&text) + && let Some(root) = root_of(path, &module) + && known_roots.insert(root.clone()) + { + read_manifest(&root, &mut component_roots, &mut fetched, manifests); + roots.push(root); } seed_texts.push(text); } @@ -2540,13 +2539,12 @@ fn resolve_imports( let Ok(text) = std::fs::read_to_string(&candidate) else { continue; }; - if let Some((m, _)) = deed_driver::imports_of(&text) { - if let Some(new_root) = root_of(&candidate, &m) { - if known_roots.insert(new_root.clone()) { - read_manifest(&new_root, &mut component_roots, &mut fetched, manifests); - roots.push(new_root); - } - } + if let Some((m, _)) = deed_driver::imports_of(&text) + && let Some(new_root) = root_of(&candidate, &m) + && known_roots.insert(new_root.clone()) + { + read_manifest(&new_root, &mut component_roots, &mut fetched, manifests); + roots.push(new_root); } known_files.insert(candidate.clone()); new_files.push(candidate.clone()); diff --git a/crates/deed-codegen/src/compile.rs b/crates/deed-codegen/src/compile.rs index 29a7260..96c4992 100644 --- a/crates/deed-codegen/src/compile.rs +++ b/crates/deed-codegen/src/compile.rs @@ -683,7 +683,7 @@ impl Strings { let mut bytes = (text.chars().count() as i64).to_le_bytes().to_vec(); bytes.extend_from_slice(&(text.len() as i64).to_le_bytes()); bytes.extend_from_slice(text.as_bytes()); - while bytes.len() % layout::WORD as usize != 0 { + while !bytes.len().is_multiple_of(layout::WORD as usize) { bytes.push(0); } self.next += bytes.len() as u32; diff --git a/crates/deed-driver/tests/explain_pages.rs b/crates/deed-driver/tests/explain_pages.rs index c362acb..440ce09 100644 --- a/crates/deed-driver/tests/explain_pages.rs +++ b/crates/deed-driver/tests/explain_pages.rs @@ -189,14 +189,13 @@ fn declared_codes(root: &Path) -> Vec<(String, String, String)> { let content = rest.strip_prefix(' ').unwrap_or(rest); doc_lines.push(content.to_string()); } else if let Some(rest) = trimmed.strip_prefix("pub const ") { - if let Some((name, rest)) = rest.split_once(':') { - if let Some(start) = rest.find("\"DEED") { - if let Some(end) = rest[start + 1..].find('"') { - let code = rest[start + 1..start + 1 + end].to_string(); - let doc = doc_lines.join("\n"); - codes.push((name.trim().to_string(), code, doc)); - } - } + if let Some((name, rest)) = rest.split_once(':') + && let Some(start) = rest.find("\"DEED") + && let Some(end) = rest[start + 1..].find('"') + { + let code = rest[start + 1..start + 1 + end].to_string(); + let doc = doc_lines.join("\n"); + codes.push((name.trim().to_string(), code, doc)); } doc_lines.clear(); } else if !trimmed.is_empty() && !trimmed.starts_with("//") { @@ -240,14 +239,13 @@ fn collect_test_files(dir: &Path, root: &Path, out: &mut Vec<(String, String)>) collect_test_files(&path, root, out); } else if path.extension().is_some_and(|e| e == "rs") && path.file_name().is_some_and(|n| n != "codes.rs") + && let Ok(text) = fs::read_to_string(&path) { - if let Ok(text) = fs::read_to_string(&path) { - let rel = path - .strip_prefix(root) - .map(|p| p.to_string_lossy().replace('\\', "/")) - .unwrap_or_else(|_| path.to_string_lossy().into_owned()); - out.push((rel, text)); - } + let rel = path + .strip_prefix(root) + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|_| path.to_string_lossy().into_owned()); + out.push((rel, text)); } } } diff --git a/crates/deed-driver/tests/publishing.rs b/crates/deed-driver/tests/publishing.rs index 2c7242d..97a024c 100644 --- a/crates/deed-driver/tests/publishing.rs +++ b/crates/deed-driver/tests/publishing.rs @@ -156,15 +156,15 @@ fn no_crate_names_a_sibling_by_path_alone() { "{} names a sibling by path, which cannot be published", path.display() ); - if let Some(name) = line.strip_suffix(".workspace = true") { - if name.starts_with("deed-") { - assert!( - names.iter().any(|declared| declared == name), - "{} inherits `{name}`, which the workspace does not declare", - path.display() - ); - inherited += 1; - } + if let Some(name) = line.strip_suffix(".workspace = true") + && name.starts_with("deed-") + { + assert!( + names.iter().any(|declared| declared == name), + "{} inherits `{name}`, which the workspace does not declare", + path.display() + ); + inherited += 1; } } } diff --git a/crates/deed-interp/src/interp.rs b/crates/deed-interp/src/interp.rs index d1a7eab..d4aa184 100644 --- a/crates/deed-interp/src/interp.rs +++ b/crates/deed-interp/src/interp.rs @@ -3378,10 +3378,8 @@ impl<'a> Interp<'a> { // Keep the original failure; only replace a non-failing outcome // with a `finally` failure. - if !body_failed { - if let Err(signal) = finally_result { - outcome = Err(signal); - } + if !body_failed && let Err(signal) = finally_result { + outcome = Err(signal); } } diff --git a/crates/deed-interp/src/property.rs b/crates/deed-interp/src/property.rs index 47f3e76..7d50b36 100644 --- a/crates/deed-interp/src/property.rs +++ b/crates/deed-interp/src/property.rs @@ -837,10 +837,10 @@ impl<'a> TypeIndex<'a> { match self.resolutions.def(def).kind { DefKind::Builtin => match name.name.as_str() { "Int" => Some(Value::Int(rng.int())), - "Bool" => Some(Value::Bool(rng.next() % 2 == 0)), + "Bool" => Some(Value::Bool(rng.next().is_multiple_of(2))), "String" => Some(Value::Str(rng.word().into())), "Result" => { - let ok = rng.next() % 2 == 0; + let ok = rng.next().is_multiple_of(2); let inner = self.generate(args.get(usize::from(!ok))?, rng, interp, depth + 1)?; Some(if ok { diff --git a/crates/deed-lsp/src/server.rs b/crates/deed-lsp/src/server.rs index 75b5a00..3a56fd5 100644 --- a/crates/deed-lsp/src/server.rs +++ b/crates/deed-lsp/src/server.rs @@ -256,11 +256,11 @@ impl Server { // A `use` path is not a resolved name and has no type. Go to definition // and documentLink already open the file; hover should still say what // module it is and whether this workspace can open it. - if range.is_none() { - if let Some((path_span, about)) = self.hover_use_path(&checked, offset) { - lines.push(about); - range = Some(path_span); - } + if range.is_none() + && let Some((path_span, about)) = self.hover_use_path(&checked, offset) + { + lines.push(about); + range = Some(path_span); } let Some(range) = range else { @@ -2557,10 +2557,10 @@ fn comment_folds(document: &Document, trivia: &[deed_lexer::Trivia], ranges: &mu for item in trivia { if item.kind != deed_lexer::TriviaKind::Line { - if let Some((start, end)) = run.take() { - if end > start { - ranges.push(comment_fold(start, end)); - } + if let Some((start, end)) = run.take() + && end > start + { + ranges.push(comment_fold(start, end)); } continue; } @@ -2581,10 +2581,10 @@ fn comment_folds(document: &Document, trivia: &[deed_lexer::Trivia], ranges: &mu }; } - if let Some((start, end)) = run { - if end > start { - ranges.push(comment_fold(start, end)); - } + if let Some((start, end)) = run + && end > start + { + ranges.push(comment_fold(start, end)); } } @@ -2703,10 +2703,8 @@ fn collect_block_spans(offset: u32, block: &Block, spans: &mut Vec) { .stmts .iter() .any(|stmt| collect_stmt_spans(offset, stmt, spans)); - if !found { - if let Some(tail) = &block.tail { - collect_expr_spans(offset, tail, spans); - } + if !found && let Some(tail) = &block.tail { + collect_expr_spans(offset, tail, spans); } push_span_if_new(block.span, spans); } @@ -2797,10 +2795,9 @@ fn collect_expr_spans(offset: u32, expr: &Expr, spans: &mut Vec) -> bool { } => { if !collect_expr_spans(offset, condition, spans) && !collect_block_spans_checked(offset, then_branch, spans) + && let Some(else_b) = else_branch { - if let Some(else_b) = else_branch { - collect_expr_spans(offset, else_b, spans); - } + collect_expr_spans(offset, else_b, spans); } } Expr::Match { diff --git a/crates/deed-mir/src/lower.rs b/crates/deed-mir/src/lower.rs index 2a3069f..711dfe1 100644 --- a/crates/deed-mir/src/lower.rs +++ b/crates/deed-mir/src/lower.rs @@ -800,10 +800,10 @@ fn instantiate_nominal( nominals: &HashMap>, shapes: &mut Vec, ) -> Result { - if args.is_empty() { - if let Some(id) = layouts.get(name) { - return Ok(*id); - } + if args.is_empty() + && let Some(id) = layouts.get(name) + { + return Ok(*id); } let nominal = match nominals.get(name) { diff --git a/crates/deed-parser/src/parser.rs b/crates/deed-parser/src/parser.rs index 0a03e71..022abd1 100644 --- a/crates/deed-parser/src/parser.rs +++ b/crates/deed-parser/src/parser.rs @@ -1220,18 +1220,18 @@ impl<'a> Parser<'a> { // disambiguate and no reason to reserve the words for the rest // of the language. if self.eat_named("state") { - if let Some(field_name) = self.expect_ident("handler state") { - if self.expect(TokenKind::Colon, "handler state").is_some() { - let ty = self.parse_type(); - if self.at(&TokenKind::Eq) { - self.state_has_no_initialiser(); - } - state.push(FieldDecl { - span: field_name.span.to(ty.span()), - name: field_name, - ty, - }); + if let Some(field_name) = self.expect_ident("handler state") + && self.expect(TokenKind::Colon, "handler state").is_some() + { + let ty = self.parse_type(); + if self.at(&TokenKind::Eq) { + self.state_has_no_initialiser(); } + state.push(FieldDecl { + span: field_name.span.to(ty.span()), + name: field_name, + ty, + }); } } else if self.eat_named("finally") { finally = Some(self.parse_block()); @@ -2334,10 +2334,10 @@ impl<'a> Parser<'a> { // `xs ++ ys` and `x :: xs`, borrowed from languages where a list has // operators. Doubled, both of them, and nothing in this grammar puts // two of either in a row, so the shape is safe to read here. - if !self.continues_a_new_line() { - if let Some(borrowed) = self.borrowed_operator() { - lhs = self.no_such_list_operator(lhs, borrowed); - } + if !self.continues_a_new_line() + && let Some(borrowed) = self.borrowed_operator() + { + lhs = self.no_such_list_operator(lhs, borrowed); } while let Some((op, bp, spelled)) = self.infix_operator() { diff --git a/crates/deed-typeck/src/check.rs b/crates/deed-typeck/src/check.rs index 10f09a0..1bc35d9 100644 --- a/crates/deed-typeck/src/check.rs +++ b/crates/deed-typeck/src/check.rs @@ -1580,13 +1580,13 @@ impl<'a> Checker<'a> { ); // The arity lives on the declaration. Without this pin the reader is // left counting angle brackets against a name that is already known. - if let Some((file, at)) = declared_here { - if !at.is_empty() { - diagnostic = match file { - Some(other) => diagnostic.with_secondary_in(other, at, "declared here"), - None => diagnostic.with_secondary(at, "declared here"), - }; - } + if let Some((file, at)) = declared_here + && !at.is_empty() + { + diagnostic = match file { + Some(other) => diagnostic.with_secondary_in(other, at, "declared here"), + None => diagnostic.with_secondary(at, "declared here"), + }; } self.emit(diagnostic); false @@ -4627,15 +4627,13 @@ impl<'a> Checker<'a> { // Same pin MISSING_FIELDS already has: the declaration is // where the real fields live, and a wrong name is the other // half of the same question. - if let Some((file, at)) = declared_here { - if !at.is_empty() { - diagnostic = match file { - Some(other) => { - diagnostic.with_secondary_in(other, at, "declared here") - } - None => diagnostic.with_secondary(at, "declared here"), - }; - } + if let Some((file, at)) = declared_here + && !at.is_empty() + { + diagnostic = match file { + Some(other) => diagnostic.with_secondary_in(other, at, "declared here"), + None => diagnostic.with_secondary(at, "declared here"), + }; } self.emit(diagnostic); if let Some(value) = &init.value { @@ -4686,13 +4684,13 @@ impl<'a> Checker<'a> { .with_note( "every field has to be given, because a partially built value is not a value", ); - if let Some((file, at)) = declared_here { - if !at.is_empty() { - diagnostic = match file { - Some(other) => diagnostic.with_secondary_in(other, at, "declared here"), - None => diagnostic.with_secondary(at, "declared here"), - }; - } + if let Some((file, at)) = declared_here + && !at.is_empty() + { + diagnostic = match file { + Some(other) => diagnostic.with_secondary_in(other, at, "declared here"), + None => diagnostic.with_secondary(at, "declared here"), + }; } self.emit(diagnostic); }