1use crate::ast::stmt::{Expr, Stmt, StringPart};
38use crate::token::Span;
39use logicaffeine_base::{Interner, Symbol};
40use std::collections::HashMap;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Severity {
44 Warning,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum FindingKind {
49 OrphanedCopy,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Finding {
56 pub kind: FindingKind,
57 pub severity: Severity,
58 pub span: Span,
59 pub message: String,
60}
61
62#[derive(Clone, PartialEq, Eq)]
67struct Place(Vec<Symbol>);
68
69fn place_of(expr: &Expr<'_>) -> Option<Place> {
70 fn chain(expr: &Expr<'_>, path: &mut Vec<Symbol>) -> bool {
71 match expr {
72 Expr::Identifier(sym) => {
73 path.push(*sym);
74 true
75 }
76 Expr::FieldAccess { object, field } => {
77 if chain(object, path) {
78 path.push(*field);
79 true
80 } else {
81 false
82 }
83 }
84 _ => false,
85 }
86 }
87 let mut path = Vec::new();
88 chain(expr, &mut path).then_some(Place(path))
89}
90
91#[derive(Clone, PartialEq, Eq)]
94enum CopySource {
95 Field(Place),
98 Local(Symbol),
101}
102
103struct Binding {
104 source: CopySource,
105 source_desc: String,
108 last_mutation: Option<Span>,
109 settled_since_mutation: bool,
112}
113
114#[derive(Default)]
116pub struct CopyLintScope {
117 bindings: HashMap<Symbol, Binding>,
118 findings: Vec<Finding>,
119}
120
121impl CopyLintScope {
122 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn observe(&mut self, span: Span, stmt: &Stmt<'_>, interner: &Interner) {
128 match stmt {
129 Stmt::Let { var, value, .. } => {
130 self.rebind(*var, interner);
131 if let Some(source) = copy_source(value) {
132 let source_desc = describe_source(&source, interner);
133 self.bindings.insert(
134 *var,
135 Binding {
136 source,
137 source_desc,
138 last_mutation: None,
139 settled_since_mutation: true,
140 },
141 );
142 } else {
143 mark_reads(value, &mut self.bindings);
144 }
145 }
146
147 Stmt::Set { target, value } => {
148 mark_reads(value, &mut self.bindings);
149 for binding in self.bindings.values_mut() {
150 if binding.source == CopySource::Local(*target) {
151 binding.settled_since_mutation = true;
152 }
153 }
154 self.rebind(*target, interner);
155 }
156
157 Stmt::SetField {
158 object,
159 field,
160 value,
161 } => {
162 mark_reads(value, &mut self.bindings);
163 let mut mutated = false;
164 if let Expr::Identifier(sym) = object {
165 if let Some(binding) = self.bindings.get_mut(sym) {
166 binding.last_mutation = Some(span);
167 binding.settled_since_mutation = false;
168 mutated = true;
169 }
170 }
171 if !mutated {
172 if let Some(mut place) = place_of(object) {
173 place.0.push(*field);
174 for binding in self.bindings.values_mut() {
175 if binding.source == CopySource::Field(place.clone()) {
176 binding.settled_since_mutation = true;
177 }
178 }
179 }
180 }
181 }
182
183 Stmt::Push { value, collection }
184 | Stmt::Add { value, collection }
185 | Stmt::Remove { value, collection } => {
186 mark_reads(value, &mut self.bindings);
187 mutate_place(collection, span, &mut self.bindings);
188 }
189 Stmt::Pop { collection, .. } => {
190 mutate_place(collection, span, &mut self.bindings);
191 }
192 Stmt::SetIndex {
193 collection,
194 index,
195 value,
196 } => {
197 mark_reads(index, &mut self.bindings);
198 mark_reads(value, &mut self.bindings);
199 mutate_place(collection, span, &mut self.bindings);
200 }
201
202 Stmt::Swap { a, b } => {
203 for sym in [a, b] {
204 if let Some(binding) = self.bindings.get_mut(sym) {
205 binding.settled_since_mutation = true;
206 }
207 }
208 }
209
210 Stmt::If { cond, .. } | Stmt::While { cond, .. } => {
211 mark_reads(cond, &mut self.bindings)
212 }
213 Stmt::Repeat { iterable, .. } => mark_reads(iterable, &mut self.bindings),
214 Stmt::Inspect { target, .. } => mark_reads(target, &mut self.bindings),
215
216 Stmt::Return { value: Some(v) } => mark_reads(v, &mut self.bindings),
217 Stmt::Show { object, recipient } | Stmt::Give { object, recipient } => {
218 mark_reads(object, &mut self.bindings);
219 mark_reads(recipient, &mut self.bindings);
220 }
221 Stmt::Call { args, .. } => {
222 for a in args {
223 mark_reads(a, &mut self.bindings);
224 }
225 }
226 Stmt::RuntimeAssert {
227 condition, reason, ..
228 } => {
229 mark_reads(condition, &mut self.bindings);
230 if let Some(r) = reason {
231 mark_reads(r, &mut self.bindings);
232 }
233 }
234 Stmt::Expect { condition } => mark_reads(condition, &mut self.bindings),
235 Stmt::Fail { reason } => mark_reads(reason, &mut self.bindings),
236 Stmt::Observe { payloads, .. } => {
237 for (_, v) in payloads {
238 mark_reads(v, &mut self.bindings);
239 }
240 }
241
242 _ => {}
243 }
244 }
245
246 fn rebind(&mut self, var: Symbol, interner: &Interner) {
250 if let Some(old) = self.bindings.remove(&var) {
251 self.finalize(var, &old, interner);
252 }
253 }
254
255 fn finalize(&mut self, var: Symbol, binding: &Binding, interner: &Interner) {
256 let Some(span) = binding.last_mutation else {
257 return;
258 };
259 if binding.settled_since_mutation {
260 return;
261 }
262 self.findings.push(Finding {
263 kind: FindingKind::OrphanedCopy,
264 severity: Severity::Warning,
265 span,
266 message: orphaned_copy_message(var, &binding.source_desc, interner),
267 });
268 }
269
270 pub fn finish(mut self, interner: &Interner) -> Vec<Finding> {
273 let remaining: Vec<(Symbol, Binding)> = self.bindings.drain().collect();
274 for (var, binding) in remaining {
275 self.finalize(var, &binding, interner);
276 }
277 self.findings
278 }
279}
280
281fn mutate_place(place: &Expr<'_>, span: Span, bindings: &mut HashMap<Symbol, Binding>) {
282 if let Expr::Identifier(sym) = place {
283 if let Some(binding) = bindings.get_mut(sym) {
284 binding.last_mutation = Some(span);
285 binding.settled_since_mutation = false;
286 }
287 }
288}
289
290fn copy_source(value: &Expr<'_>) -> Option<CopySource> {
291 match value {
292 Expr::FieldAccess { .. } => place_of(value).map(CopySource::Field),
293 Expr::Identifier(sym) => Some(CopySource::Local(*sym)),
294 _ => None,
295 }
296}
297
298fn describe_source(source: &CopySource, interner: &Interner) -> String {
299 match source {
300 CopySource::Field(place) => place
301 .0
302 .iter()
303 .map(|s| interner.resolve(*s))
304 .collect::<Vec<_>>()
305 .join("'s "),
306 CopySource::Local(sym) => interner.resolve(*sym).to_string(),
307 }
308}
309
310fn orphaned_copy_message(var: Symbol, source_desc: &str, interner: &Interner) -> String {
311 let var_str = interner.resolve(var);
312 format!(
313 "'{var_str}' is a copy of '{source_desc}' — collections and records are values in LOGOS, \
314 so this mutates the copy, never '{source_desc}' itself. Either write it back (`Set \
315 {source_desc} to {var_str}.`), or mutate '{source_desc}' directly."
316 )
317}
318
319fn mark_reads(expr: &Expr<'_>, bindings: &mut HashMap<Symbol, Binding>) {
324 match expr {
325 Expr::Identifier(sym) => {
326 if let Some(binding) = bindings.get_mut(sym) {
327 binding.settled_since_mutation = true;
328 }
329 }
330 Expr::FieldAccess { object, .. } => mark_reads(object, bindings),
331 Expr::BinaryOp { left, right, .. } => {
332 mark_reads(left, bindings);
333 mark_reads(right, bindings);
334 }
335 Expr::Not { operand } | Expr::Copy { expr: operand } | Expr::Give { value: operand } => {
336 mark_reads(operand, bindings);
337 }
338 Expr::Call { args, .. } => {
339 for a in args {
340 mark_reads(a, bindings);
341 }
342 }
343 Expr::CallExpr { callee, args } => {
344 mark_reads(callee, bindings);
345 for a in args {
346 mark_reads(a, bindings);
347 }
348 }
349 Expr::Index { collection, index } => {
350 mark_reads(collection, bindings);
351 mark_reads(index, bindings);
352 }
353 Expr::Slice {
354 collection,
355 start,
356 end,
357 } => {
358 mark_reads(collection, bindings);
359 mark_reads(start, bindings);
360 mark_reads(end, bindings);
361 }
362 Expr::Length { collection } | Expr::ManifestOf { zone: collection } => {
363 mark_reads(collection, bindings);
364 }
365 Expr::Contains { collection, value } => {
366 mark_reads(collection, bindings);
367 mark_reads(value, bindings);
368 }
369 Expr::Union { left, right } | Expr::Intersection { left, right } => {
370 mark_reads(left, bindings);
371 mark_reads(right, bindings);
372 }
373 Expr::ChunkAt { index, zone } => {
374 mark_reads(index, bindings);
375 mark_reads(zone, bindings);
376 }
377 Expr::List(items) | Expr::Tuple(items) => {
378 for i in items {
379 mark_reads(i, bindings);
380 }
381 }
382 Expr::Range { start, end } => {
383 mark_reads(start, bindings);
384 mark_reads(end, bindings);
385 }
386 Expr::New { init_fields, .. } => {
387 for (_, v) in init_fields {
388 mark_reads(v, bindings);
389 }
390 }
391 Expr::NewVariant { fields, .. } => {
392 for (_, v) in fields {
393 mark_reads(v, bindings);
394 }
395 }
396 Expr::OptionSome { value } => mark_reads(value, bindings),
397 Expr::WithCapacity { value, capacity } => {
398 mark_reads(value, bindings);
399 mark_reads(capacity, bindings);
400 }
401 Expr::InterpolatedString(parts) => {
402 for part in parts.iter() {
403 if let StringPart::Expr { value, .. } = part {
404 mark_reads(value, bindings);
405 }
406 }
407 }
408 _ => {}
409 }
410}