1use crate::canonical::{
37 CExpr, Effect, FnDecl, Param, Pattern, Stage, TypeExpr,
38};
39use crate::ids::NodeId;
40
41#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
42#[serde(tag = "kind", rename_all = "snake_case")]
43pub enum TransformError {
44 #[error("unknown node id `{at}`")]
45 UnknownNode { at: String },
46 #[error("expected a Match expression at `{at}` but found `{found_kind}`")]
47 NotAMatch { at: String, found_kind: &'static str },
48 #[error("expected a Let expression at `{at}` but found `{found_kind}`")]
49 NotALet { at: String, found_kind: &'static str },
50 #[error("arm index {requested} out of range (arm count = {arm_count}) at `{at}`")]
51 ArmIndexOutOfRange { at: String, arm_count: usize, requested: usize },
52 #[error("malformed NodeId `{0}`")]
53 BadNodeId(String),
54 #[error("cannot transform inside `{stage_kind}` — only FnDecl bodies are transformable")]
55 NonFnTarget { stage_kind: &'static str },
56 #[error("rename is a no-op: old and new name are both `{name}`")]
57 RenameNoOp { name: String },
58 #[error("inline_let refused: `{reason}`")]
59 InlineLetRefused { reason: String },
60 #[error("extract_function refused: `{reason}`")]
61 ExtractFnRefused { reason: String },
62}
63
64#[derive(Debug, Clone, PartialEq)]
71pub struct ExtractFnSpec {
72 pub name: String,
73 pub type_params: Vec<String>,
74 pub params: Vec<Param>,
75 pub return_type: TypeExpr,
76 pub effects: Vec<Effect>,
77}
78
79pub fn replace_match_arm(
88 stage: &Stage,
89 match_node: &NodeId,
90 arm_index: usize,
91 new_body: CExpr,
92) -> Result<Stage, TransformError> {
93 let mut out = stage.clone();
94 let (body, n_params) = match &mut out {
95 Stage::FnDecl(fd) => {
96 let n = fd.params.len();
97 (&mut fd.body, n)
98 }
99 Stage::TypeDecl(_) => return Err(TransformError::NonFnTarget { stage_kind: "TypeDecl" }),
100 Stage::Import(_) => return Err(TransformError::NonFnTarget { stage_kind: "Import" }),
101 };
102 let path = parse_node_id(match_node.as_str())?;
103 if path.is_empty() {
107 return Err(TransformError::NotAMatch {
108 at: match_node.as_str().into(),
109 found_kind: "stage_root",
110 });
111 }
112 if path[0] != n_params + 1 {
113 return Err(TransformError::UnknownNode { at: match_node.as_str().into() });
114 }
115 let inner = &path[1..];
116 let target = navigate_to_expr(body, inner, match_node.as_str())?;
117 let CExpr::Match { scrutinee: _, arms } = target else {
118 return Err(TransformError::NotAMatch {
119 at: match_node.as_str().into(),
120 found_kind: cexpr_kind(target),
121 });
122 };
123 if arm_index >= arms.len() {
124 return Err(TransformError::ArmIndexOutOfRange {
125 at: match_node.as_str().into(),
126 arm_count: arms.len(),
127 requested: arm_index,
128 });
129 }
130 arms[arm_index].body = new_body;
131 Ok(out)
132}
133
134pub fn rename_local(
153 stage: &Stage,
154 let_node: &NodeId,
155 new_name: &str,
156) -> Result<Stage, TransformError> {
157 let mut out = stage.clone();
158 let (body, n_params) = match &mut out {
159 Stage::FnDecl(fd) => {
160 let n = fd.params.len();
161 (&mut fd.body, n)
162 }
163 Stage::TypeDecl(_) => return Err(TransformError::NonFnTarget { stage_kind: "TypeDecl" }),
164 Stage::Import(_) => return Err(TransformError::NonFnTarget { stage_kind: "Import" }),
165 };
166 let path = parse_node_id(let_node.as_str())?;
167 if path.is_empty() {
168 return Err(TransformError::NotALet {
169 at: let_node.as_str().into(),
170 found_kind: "stage_root",
171 });
172 }
173 if path[0] != n_params + 1 {
174 return Err(TransformError::UnknownNode { at: let_node.as_str().into() });
175 }
176 let inner = &path[1..];
177 let target = navigate_to_expr(body, inner, let_node.as_str())?;
178 let CExpr::Let { name, body: let_body, .. } = target else {
179 return Err(TransformError::NotALet {
180 at: let_node.as_str().into(),
181 found_kind: cexpr_kind(target),
182 });
183 };
184 if name == new_name {
185 return Err(TransformError::RenameNoOp { name: name.clone() });
186 }
187 let old_name = std::mem::replace(name, new_name.to_string());
188 rewrite_var_in_expr(let_body, &old_name, new_name);
190 Ok(out)
191}
192
193pub fn inline_let(
214 stage: &Stage,
215 let_node: &NodeId,
216) -> Result<Stage, TransformError> {
217 let mut out = stage.clone();
218 let (body, n_params) = match &mut out {
219 Stage::FnDecl(fd) => {
220 let n = fd.params.len();
221 (&mut fd.body, n)
222 }
223 Stage::TypeDecl(_) => return Err(TransformError::NonFnTarget { stage_kind: "TypeDecl" }),
224 Stage::Import(_) => return Err(TransformError::NonFnTarget { stage_kind: "Import" }),
225 };
226 let path = parse_node_id(let_node.as_str())?;
227 if path.is_empty() {
228 return Err(TransformError::NotALet {
229 at: let_node.as_str().into(),
230 found_kind: "stage_root",
231 });
232 }
233 if path[0] != n_params + 1 {
234 return Err(TransformError::UnknownNode { at: let_node.as_str().into() });
235 }
236 let inner = &path[1..];
237 if inner.is_empty() {
241 let CExpr::Let { name, value, body: let_body, .. } = body.clone() else {
242 return Err(TransformError::NotALet {
243 at: let_node.as_str().into(),
244 found_kind: cexpr_kind(body),
245 });
246 };
247 check_inlinable(&value)?;
248 let captures = free_vars(&value);
249 check_no_capture(&let_body, &captures)?;
250 let mut replaced = *let_body;
251 substitute_in_expr(&mut replaced, &name, &value);
252 *body = replaced;
253 return Ok(out);
254 }
255 let target = navigate_to_expr(body, inner, let_node.as_str())?;
260 let CExpr::Let { name, value, body: let_body, .. } = target.clone() else {
261 return Err(TransformError::NotALet {
262 at: let_node.as_str().into(),
263 found_kind: cexpr_kind(target),
264 });
265 };
266 check_inlinable(&value)?;
267 let captures = free_vars(&value);
268 check_no_capture(&let_body, &captures)?;
269 let mut replaced = *let_body;
270 substitute_in_expr(&mut replaced, &name, &value);
271 *target = replaced;
272 Ok(out)
273}
274
275fn check_inlinable(v: &CExpr) -> Result<(), TransformError> {
280 match v {
281 CExpr::Literal { .. } | CExpr::Var { .. } => Ok(()),
282 CExpr::FieldAccess { value, .. } => check_inlinable(value),
283 CExpr::BinOp { lhs, rhs, .. } => {
284 check_inlinable(lhs)?;
285 check_inlinable(rhs)
286 }
287 CExpr::UnaryOp { expr, .. } => check_inlinable(expr),
288 CExpr::TupleLit { items } | CExpr::ListLit { items } => {
289 for it in items { check_inlinable(it)?; }
290 Ok(())
291 }
292 other => Err(TransformError::InlineLetRefused {
293 reason: format!(
294 "let value contains a `{}` expression; slice 3 only inlines literal/var/field/binop/unaryop/tuple/list trees",
295 cexpr_kind(other)
296 ),
297 }),
298 }
299}
300
301fn free_vars(v: &CExpr) -> std::collections::BTreeSet<String> {
304 let mut out = std::collections::BTreeSet::new();
305 collect_free_vars(v, &mut out);
306 out
307}
308
309fn collect_free_vars(e: &CExpr, out: &mut std::collections::BTreeSet<String>) {
310 match e {
311 CExpr::Var { name } => { out.insert(name.clone()); }
312 CExpr::Literal { .. } => {}
313 CExpr::Call { callee, args } => {
314 collect_free_vars(callee, out);
315 for a in args { collect_free_vars(a, out); }
316 }
317 CExpr::Let { value, body, name, .. } => {
318 collect_free_vars(value, out);
319 let mut inner = std::collections::BTreeSet::new();
322 collect_free_vars(body, &mut inner);
323 inner.remove(name);
324 out.extend(inner);
325 }
326 CExpr::Match { scrutinee, arms } => {
327 collect_free_vars(scrutinee, out);
328 for arm in arms {
329 let mut inner = std::collections::BTreeSet::new();
330 collect_free_vars(&arm.body, &mut inner);
331 let bound = pattern_bindings(&arm.pattern);
332 for b in bound { inner.remove(&b); }
333 out.extend(inner);
334 }
335 }
336 CExpr::Block { statements, result } => {
337 for s in statements { collect_free_vars(s, out); }
338 collect_free_vars(result, out);
339 }
340 CExpr::Constructor { args, .. } => {
341 for a in args { collect_free_vars(a, out); }
342 }
343 CExpr::RecordLit { fields } => {
344 for f in fields { collect_free_vars(&f.value, out); }
345 }
346 CExpr::TupleLit { items } | CExpr::ListLit { items } => {
347 for i in items { collect_free_vars(i, out); }
348 }
349 CExpr::FieldAccess { value, .. } => collect_free_vars(value, out),
350 CExpr::Lambda { params, body, .. } => {
351 let mut inner = std::collections::BTreeSet::new();
352 collect_free_vars(body, &mut inner);
353 for p in params { inner.remove(&p.name); }
354 out.extend(inner);
355 }
356 CExpr::BinOp { lhs, rhs, .. } => {
357 collect_free_vars(lhs, out);
358 collect_free_vars(rhs, out);
359 }
360 CExpr::UnaryOp { expr, .. } => collect_free_vars(expr, out),
361 CExpr::Return { value } => collect_free_vars(value, out),
362 }
363}
364
365fn pattern_bindings(p: &Pattern) -> Vec<String> {
366 let mut out = Vec::new();
367 collect_pattern_bindings(p, &mut out);
368 out
369}
370
371fn collect_pattern_bindings(p: &Pattern, out: &mut Vec<String>) {
372 match p {
373 Pattern::PVar { name } => out.push(name.clone()),
374 Pattern::PLiteral { .. } | Pattern::PWild => {}
375 Pattern::PConstructor { args, .. } => for p in args { collect_pattern_bindings(p, out); }
376 Pattern::PRecord { fields } => for f in fields { collect_pattern_bindings(&f.pattern, out); }
377 Pattern::PTuple { items } => for p in items { collect_pattern_bindings(p, out); }
378 }
379}
380
381fn check_no_capture(
387 body: &CExpr,
388 captures: &std::collections::BTreeSet<String>,
389) -> Result<(), TransformError> {
390 let mut conflict: Option<String> = None;
391 walk_binders(body, &mut |name| {
392 if captures.contains(name) && conflict.is_none() {
393 conflict = Some(name.to_string());
394 }
395 });
396 if let Some(name) = conflict {
397 return Err(TransformError::InlineLetRefused {
398 reason: format!(
399 "value's free var `{name}` is re-bound in the body; inlining would capture"
400 ),
401 });
402 }
403 Ok(())
404}
405
406fn walk_binders(e: &CExpr, on_binder: &mut dyn FnMut(&str)) {
407 match e {
408 CExpr::Let { name, value, body, .. } => {
409 on_binder(name);
410 walk_binders(value, on_binder);
411 walk_binders(body, on_binder);
412 }
413 CExpr::Lambda { params, body, .. } => {
414 for p in params { on_binder(&p.name); }
415 walk_binders(body, on_binder);
416 }
417 CExpr::Match { scrutinee, arms } => {
418 walk_binders(scrutinee, on_binder);
419 for arm in arms {
420 for b in pattern_bindings(&arm.pattern) { on_binder(&b); }
421 walk_binders(&arm.body, on_binder);
422 }
423 }
424 CExpr::Call { callee, args } => {
425 walk_binders(callee, on_binder);
426 for a in args { walk_binders(a, on_binder); }
427 }
428 CExpr::Block { statements, result } => {
429 for s in statements { walk_binders(s, on_binder); }
430 walk_binders(result, on_binder);
431 }
432 CExpr::Constructor { args, .. } => for a in args { walk_binders(a, on_binder); }
433 CExpr::RecordLit { fields } => for f in fields { walk_binders(&f.value, on_binder); }
434 CExpr::TupleLit { items } | CExpr::ListLit { items } => {
435 for i in items { walk_binders(i, on_binder); }
436 }
437 CExpr::FieldAccess { value, .. } => walk_binders(value, on_binder),
438 CExpr::BinOp { lhs, rhs, .. } => {
439 walk_binders(lhs, on_binder); walk_binders(rhs, on_binder);
440 }
441 CExpr::UnaryOp { expr, .. } => walk_binders(expr, on_binder),
442 CExpr::Return { value } => walk_binders(value, on_binder),
443 CExpr::Var { .. } | CExpr::Literal { .. } => {}
444 }
445}
446
447fn substitute_in_expr(e: &mut CExpr, name: &str, replacement: &CExpr) {
452 match e {
453 CExpr::Var { name: n } if n == name => {
454 *e = replacement.clone();
455 }
456 CExpr::Var { .. } | CExpr::Literal { .. } => {}
457 CExpr::Call { callee, args } => {
458 substitute_in_expr(callee, name, replacement);
459 for a in args { substitute_in_expr(a, name, replacement); }
460 }
461 CExpr::Let { name: binder, value, body, .. } => {
462 substitute_in_expr(value, name, replacement);
463 if binder != name {
464 substitute_in_expr(body, name, replacement);
465 }
466 }
467 CExpr::Match { scrutinee, arms } => {
468 substitute_in_expr(scrutinee, name, replacement);
469 for arm in arms {
470 if !pattern_binds(&arm.pattern, name) {
471 substitute_in_expr(&mut arm.body, name, replacement);
472 }
473 }
474 }
475 CExpr::Block { statements, result } => {
476 for s in statements { substitute_in_expr(s, name, replacement); }
477 substitute_in_expr(result, name, replacement);
478 }
479 CExpr::Constructor { args, .. } => {
480 for a in args { substitute_in_expr(a, name, replacement); }
481 }
482 CExpr::RecordLit { fields } => {
483 for f in fields { substitute_in_expr(&mut f.value, name, replacement); }
484 }
485 CExpr::TupleLit { items } | CExpr::ListLit { items } => {
486 for i in items { substitute_in_expr(i, name, replacement); }
487 }
488 CExpr::FieldAccess { value, .. } => substitute_in_expr(value, name, replacement),
489 CExpr::Lambda { params, body, .. } => {
490 if !params.iter().any(|p| p.name == name) {
491 substitute_in_expr(body, name, replacement);
492 }
493 }
494 CExpr::BinOp { lhs, rhs, .. } => {
495 substitute_in_expr(lhs, name, replacement);
496 substitute_in_expr(rhs, name, replacement);
497 }
498 CExpr::UnaryOp { expr, .. } => substitute_in_expr(expr, name, replacement),
499 CExpr::Return { value } => substitute_in_expr(value, name, replacement),
500 }
501}
502
503fn rewrite_var_in_expr(e: &mut CExpr, old: &str, new: &str) {
504 match e {
505 CExpr::Var { name } => {
506 if name == old { *name = new.into(); }
507 }
508 CExpr::Literal { .. } => {}
509 CExpr::Call { callee, args } => {
510 rewrite_var_in_expr(callee, old, new);
511 for a in args { rewrite_var_in_expr(a, old, new); }
512 }
513 CExpr::Let { name, value, body, .. } => {
514 rewrite_var_in_expr(value, old, new);
517 if name != old {
520 rewrite_var_in_expr(body, old, new);
521 }
522 }
523 CExpr::Match { scrutinee, arms } => {
524 rewrite_var_in_expr(scrutinee, old, new);
525 for arm in arms {
526 if !pattern_binds(&arm.pattern, old) {
527 rewrite_var_in_expr(&mut arm.body, old, new);
528 }
529 }
530 }
531 CExpr::Block { statements, result } => {
532 for s in statements { rewrite_var_in_expr(s, old, new); }
533 rewrite_var_in_expr(result, old, new);
534 }
535 CExpr::Constructor { args, .. } => {
536 for a in args { rewrite_var_in_expr(a, old, new); }
537 }
538 CExpr::RecordLit { fields } => {
539 for f in fields { rewrite_var_in_expr(&mut f.value, old, new); }
540 }
541 CExpr::TupleLit { items } | CExpr::ListLit { items } => {
542 for i in items { rewrite_var_in_expr(i, old, new); }
543 }
544 CExpr::FieldAccess { value, .. } => rewrite_var_in_expr(value, old, new),
545 CExpr::Lambda { params, body, .. } => {
546 if !params.iter().any(|p| p.name == old) {
548 rewrite_var_in_expr(body, old, new);
549 }
550 }
551 CExpr::BinOp { lhs, rhs, .. } => {
552 rewrite_var_in_expr(lhs, old, new);
553 rewrite_var_in_expr(rhs, old, new);
554 }
555 CExpr::UnaryOp { expr, .. } => rewrite_var_in_expr(expr, old, new),
556 CExpr::Return { value } => rewrite_var_in_expr(value, old, new),
557 }
558}
559
560fn pattern_binds(p: &Pattern, name: &str) -> bool {
561 match p {
562 Pattern::PVar { name: n } => n == name,
563 Pattern::PLiteral { .. } | Pattern::PWild => false,
564 Pattern::PConstructor { args, .. } => args.iter().any(|p| pattern_binds(p, name)),
565 Pattern::PRecord { fields } => fields.iter().any(|f| pattern_binds(&f.pattern, name)),
566 Pattern::PTuple { items } => items.iter().any(|p| pattern_binds(p, name)),
567 }
568}
569
570pub fn extract_function(
585 stage: &Stage,
586 expr_node: &NodeId,
587 spec: ExtractFnSpec,
588) -> Result<(Stage, Stage), TransformError> {
589 let mut modified = stage.clone();
592 let (body, n_params) = match &mut modified {
593 Stage::FnDecl(fd) => {
594 let n = fd.params.len();
595 (&mut fd.body, n)
596 }
597 Stage::TypeDecl(_) => return Err(TransformError::NonFnTarget { stage_kind: "TypeDecl" }),
598 Stage::Import(_) => return Err(TransformError::NonFnTarget { stage_kind: "Import" }),
599 };
600 let path = parse_node_id(expr_node.as_str())?;
601 if path.is_empty() {
602 return Err(TransformError::UnknownNode { at: expr_node.as_str().into() });
603 }
604 if path[0] != n_params + 1 {
605 return Err(TransformError::UnknownNode { at: expr_node.as_str().into() });
606 }
607 let inner = &path[1..];
608 let target = navigate_to_expr(body, inner, expr_node.as_str())?;
609
610 let extracted_expr = target.clone();
614
615 let free = free_vars(&extracted_expr);
619 let declared: std::collections::BTreeSet<String> =
620 spec.params.iter().map(|p| p.name.clone()).collect();
621 if free != declared {
622 let only_in_free: Vec<&String> = free.difference(&declared).collect();
623 let only_in_declared: Vec<&String> = declared.difference(&free).collect();
624 return Err(TransformError::ExtractFnRefused {
625 reason: format!(
626 "free vars {free:?} differ from declared params {declared:?}: \
627 missing {only_in_free:?}, extra {only_in_declared:?}"
628 ),
629 });
630 }
631
632 let call = CExpr::Call {
636 callee: Box::new(CExpr::Var { name: spec.name.clone() }),
637 args: spec.params.iter()
638 .map(|p| CExpr::Var { name: p.name.clone() })
639 .collect(),
640 };
641 *target = call;
642
643 let new_fn = Stage::FnDecl(FnDecl {
645 name: spec.name,
646 type_params: spec.type_params,
647 params: spec.params,
648 effects: spec.effects,
649 return_type: spec.return_type,
650 body: extracted_expr,
651 examples: Vec::new(),
652 });
653
654 Ok((modified, new_fn))
655}
656
657fn parse_node_id(id: &str) -> Result<Vec<usize>, TransformError> {
658 let s = id.strip_prefix("n_").ok_or_else(|| TransformError::BadNodeId(id.into()))?;
659 let mut parts = s.split('.');
660 let head = parts.next().ok_or_else(|| TransformError::BadNodeId(id.into()))?;
661 if head != "0" {
662 return Err(TransformError::BadNodeId(id.into()));
663 }
664 let mut out = Vec::new();
665 for p in parts {
666 out.push(p.parse::<usize>().map_err(|_| TransformError::BadNodeId(id.into()))?);
667 }
668 Ok(out)
669}
670
671fn navigate_to_expr<'a>(
677 root: &'a mut CExpr,
678 path: &[usize],
679 target_id: &str,
680) -> Result<&'a mut CExpr, TransformError> {
681 let mut current = root;
682 for &idx in path {
683 current = step_expr(current, idx)
684 .ok_or_else(|| TransformError::UnknownNode { at: target_id.into() })?;
685 }
686 Ok(current)
687}
688
689fn step_expr(e: &mut CExpr, idx: usize) -> Option<&mut CExpr> {
693 match e {
694 CExpr::Call { callee, args } => {
695 if idx == 0 { return Some(callee); }
696 args.get_mut(idx - 1)
697 }
698 CExpr::Let { value, body, .. } => {
699 match idx {
700 0 => Some(value),
701 1 => Some(body),
702 _ => None,
703 }
704 }
705 CExpr::Match { scrutinee, arms } => {
706 if idx == 0 { return Some(scrutinee); }
707 let arm_off = idx - 1;
712 if arm_off % 2 != 1 {
713 return None;
714 }
715 let arm_index = arm_off / 2;
716 arms.get_mut(arm_index).map(|a| &mut a.body)
717 }
718 CExpr::Block { statements, result } => {
719 if idx < statements.len() {
720 statements.get_mut(idx)
721 } else if idx == statements.len() {
722 Some(result)
723 } else {
724 None
725 }
726 }
727 CExpr::Constructor { args, .. } | CExpr::TupleLit { items: args, .. }
728 | CExpr::ListLit { items: args, .. } => args.get_mut(idx),
729 CExpr::RecordLit { fields } => fields.get_mut(idx).map(|f| &mut f.value),
730 CExpr::FieldAccess { value, .. } => if idx == 0 { Some(value) } else { None },
731 CExpr::Lambda { body, .. } => if idx == 0 { Some(body) } else { None },
732 CExpr::BinOp { lhs, rhs, .. } => match idx {
733 0 => Some(lhs), 1 => Some(rhs), _ => None,
734 },
735 CExpr::UnaryOp { expr, .. } => if idx == 0 { Some(expr) } else { None },
736 CExpr::Return { value } => if idx == 0 { Some(value) } else { None },
737 _ => None,
738 }
739}
740
741fn cexpr_kind(e: &CExpr) -> &'static str {
742 match e {
743 CExpr::Literal { .. } => "Literal",
744 CExpr::Var { .. } => "Var",
745 CExpr::Call { .. } => "Call",
746 CExpr::Let { .. } => "Let",
747 CExpr::Match { .. } => "Match",
748 CExpr::Block { .. } => "Block",
749 CExpr::Constructor { .. } => "Constructor",
750 CExpr::RecordLit { .. } => "RecordLit",
751 CExpr::TupleLit { .. } => "TupleLit",
752 CExpr::ListLit { .. } => "ListLit",
753 CExpr::FieldAccess { .. } => "FieldAccess",
754 CExpr::Lambda { .. } => "Lambda",
755 CExpr::BinOp { .. } => "BinOp",
756 CExpr::UnaryOp { .. } => "UnaryOp",
757 CExpr::Return { .. } => "Return",
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764 use crate::canonical::{Arm, CLit, FnDecl, Param, Pattern, TypeExpr};
765
766 fn let_stage() -> Stage {
770 let body = CExpr::Let {
771 name: "x".into(),
772 ty: None,
773 value: Box::new(CExpr::BinOp {
774 op: "+".into(),
775 lhs: Box::new(CExpr::Var { name: "n".into() }),
776 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
777 }),
778 body: Box::new(CExpr::BinOp {
779 op: "+".into(),
780 lhs: Box::new(CExpr::Var { name: "x".into() }),
781 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
782 }),
783 };
784 Stage::FnDecl(FnDecl {
785 name: "outer".into(),
786 type_params: Vec::new(),
787 params: vec![Param {
788 name: "n".into(),
789 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
790 }],
791 effects: Vec::new(),
792 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
793 body,
794 examples: Vec::new(),
795 })
796 }
797
798 fn let_node_id() -> NodeId { NodeId("n_0.2".into()) }
799
800 #[test]
801 fn rename_local_renames_binding_and_body_reference() {
802 let stage = let_stage();
803 let out = rename_local(&stage, &let_node_id(), "y").unwrap();
804 let Stage::FnDecl(fd) = out else { panic!() };
805 let CExpr::Let { name, value, body, .. } = fd.body else { panic!() };
806 assert_eq!(name, "y", "binding renamed");
807 let CExpr::BinOp { lhs, .. } = *value else { panic!() };
809 assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "n"));
810 let CExpr::BinOp { lhs, .. } = *body else { panic!() };
812 assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "y"));
813 }
814
815 #[test]
816 fn rename_local_refuses_no_op() {
817 let stage = let_stage();
818 let err = rename_local(&stage, &let_node_id(), "x").unwrap_err();
819 assert!(matches!(err, TransformError::RenameNoOp { .. }));
820 }
821
822 #[test]
823 fn rename_local_respects_inner_let_shadowing() {
824 let inner = CExpr::Let {
828 name: "x".into(),
829 ty: None,
830 value: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
831 body: Box::new(CExpr::Var { name: "x".into() }),
832 };
833 let body = CExpr::Let {
834 name: "x".into(),
835 ty: None,
836 value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
837 body: Box::new(inner),
838 };
839 let stage = Stage::FnDecl(FnDecl {
840 name: "f".into(),
841 type_params: Vec::new(),
842 params: Vec::new(),
843 effects: Vec::new(),
844 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
845 body,
846 examples: Vec::new(),
847 });
848 let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
849 let Stage::FnDecl(fd) = out else { panic!() };
850 let CExpr::Let { name: outer_name, body: outer_body, .. } = fd.body else { panic!() };
851 assert_eq!(outer_name, "y", "outer let renamed");
852 let CExpr::Let { name: inner_name, body: inner_body, .. } = *outer_body else { panic!() };
853 assert_eq!(inner_name, "x");
855 assert!(matches!(*inner_body, CExpr::Var { name: ref n } if n == "x"));
857 }
858
859 #[test]
860 fn rename_local_respects_lambda_param_shadowing() {
861 let lambda = CExpr::Lambda {
863 params: vec![Param {
864 name: "x".into(),
865 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
866 }],
867 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
868 effects: Vec::new(),
869 body: Box::new(CExpr::Var { name: "x".into() }),
870 };
871 let body = CExpr::Let {
872 name: "x".into(),
873 ty: None,
874 value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
875 body: Box::new(lambda),
876 };
877 let stage = Stage::FnDecl(FnDecl {
878 name: "f".into(),
879 type_params: Vec::new(),
880 params: Vec::new(),
881 effects: Vec::new(),
882 return_type: TypeExpr::Function {
883 params: vec![TypeExpr::Named { name: "Int".into(), args: Vec::new() }],
884 effects: Vec::new(),
885 ret: Box::new(TypeExpr::Named { name: "Int".into(), args: Vec::new() }),
886 },
887 body,
888 examples: Vec::new(),
889 });
890 let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
891 let Stage::FnDecl(fd) = out else { panic!() };
892 let CExpr::Let { name, body: outer_body, .. } = fd.body else { panic!() };
893 assert_eq!(name, "y");
894 let CExpr::Lambda { body: lam_body, .. } = *outer_body else { panic!() };
895 assert!(matches!(*lam_body, CExpr::Var { name: ref n } if n == "x"));
897 }
898
899 #[test]
900 fn rename_local_respects_match_pattern_shadowing() {
901 let match_expr = CExpr::Match {
908 scrutinee: Box::new(CExpr::Var { name: "foo".into() }),
909 arms: vec![
910 Arm {
911 pattern: Pattern::PVar { name: "x".into() },
912 body: CExpr::Var { name: "x".into() },
913 },
914 Arm {
915 pattern: Pattern::PWild,
916 body: CExpr::Var { name: "x".into() },
917 },
918 ],
919 };
920 let body = CExpr::Let {
921 name: "x".into(),
922 ty: None,
923 value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
924 body: Box::new(match_expr),
925 };
926 let stage = Stage::FnDecl(FnDecl {
927 name: "f".into(),
928 type_params: Vec::new(),
929 params: Vec::new(),
930 effects: Vec::new(),
931 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
932 body,
933 examples: Vec::new(),
934 });
935 let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
936 let Stage::FnDecl(fd) = out else { panic!() };
937 let CExpr::Let { body: outer_body, .. } = fd.body else { panic!() };
938 let CExpr::Match { arms, .. } = *outer_body else { panic!() };
939 assert!(matches!(arms[0].body, CExpr::Var { name: ref n } if n == "x"));
941 assert!(matches!(arms[1].body, CExpr::Var { name: ref n } if n == "y"));
943 }
944
945 #[test]
946 fn rename_local_not_a_let_errors() {
947 let stage = match_stage_with_two_arms();
950 let err = rename_local(&stage, &NodeId("n_0.2".into()), "y").unwrap_err();
951 assert!(matches!(err, TransformError::NotALet { found_kind: "Match", .. }),
952 "got {err:?}");
953 }
954
955 fn inlinable_stage() -> Stage {
959 let body = CExpr::Let {
960 name: "x".into(),
961 ty: None,
962 value: Box::new(CExpr::Literal { value: CLit::Int { value: 5 } }),
963 body: Box::new(CExpr::BinOp {
964 op: "+".into(),
965 lhs: Box::new(CExpr::Var { name: "x".into() }),
966 rhs: Box::new(CExpr::Var { name: "n".into() }),
967 }),
968 };
969 Stage::FnDecl(FnDecl {
970 name: "f".into(),
971 type_params: Vec::new(),
972 params: vec![Param {
973 name: "n".into(),
974 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
975 }],
976 effects: Vec::new(),
977 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
978 body,
979 examples: Vec::new(),
980 })
981 }
982
983 #[test]
984 fn inline_let_substitutes_literal_value() {
985 let stage = inlinable_stage();
986 let out = inline_let(&stage, &NodeId("n_0.2".into())).unwrap();
987 let Stage::FnDecl(fd) = out else { panic!() };
988 let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
990 assert!(matches!(*lhs, CExpr::Literal { value: CLit::Int { value: 5 } }));
991 }
992
993 #[test]
994 fn inline_let_refuses_call_in_value() {
995 let body = CExpr::Let {
997 name: "x".into(),
998 ty: None,
999 value: Box::new(CExpr::Call {
1000 callee: Box::new(CExpr::Var { name: "f".into() }),
1001 args: Vec::new(),
1002 }),
1003 body: Box::new(CExpr::Var { name: "x".into() }),
1004 };
1005 let stage = Stage::FnDecl(FnDecl {
1006 name: "g".into(),
1007 type_params: Vec::new(),
1008 params: Vec::new(),
1009 effects: Vec::new(),
1010 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1011 body,
1012 examples: Vec::new(),
1013 });
1014 let err = inline_let(&stage, &NodeId("n_0.1".into())).unwrap_err();
1015 assert!(matches!(err, TransformError::InlineLetRefused { .. }), "got {err:?}");
1016 }
1017
1018 #[test]
1019 fn inline_let_refuses_capture() {
1020 let inner = CExpr::Let {
1023 name: "y".into(),
1024 ty: None,
1025 value: Box::new(CExpr::Literal { value: CLit::Int { value: 7 } }),
1026 body: Box::new(CExpr::BinOp {
1027 op: "+".into(),
1028 lhs: Box::new(CExpr::Var { name: "x".into() }),
1029 rhs: Box::new(CExpr::Var { name: "y".into() }),
1030 }),
1031 };
1032 let body = CExpr::Let {
1033 name: "x".into(),
1034 ty: None,
1035 value: Box::new(CExpr::Var { name: "y".into() }),
1036 body: Box::new(inner),
1037 };
1038 let stage = Stage::FnDecl(FnDecl {
1039 name: "g".into(),
1040 type_params: Vec::new(),
1041 params: vec![Param {
1042 name: "y".into(),
1043 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1044 }],
1045 effects: Vec::new(),
1046 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1047 body,
1048 examples: Vec::new(),
1049 });
1050 let err = inline_let(&stage, &NodeId("n_0.2".into())).unwrap_err();
1052 assert!(matches!(err, TransformError::InlineLetRefused { .. }), "got {err:?}");
1053 }
1054
1055 #[test]
1056 fn inline_let_substitutes_under_shadowing() {
1057 let inner = CExpr::Let {
1062 name: "x".into(),
1063 ty: None,
1064 value: Box::new(CExpr::Var { name: "n".into() }),
1065 body: Box::new(CExpr::BinOp {
1066 op: "+".into(),
1067 lhs: Box::new(CExpr::Var { name: "x".into() }),
1068 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
1069 }),
1070 };
1071 let body = CExpr::Let {
1072 name: "x".into(),
1073 ty: None,
1074 value: Box::new(CExpr::Literal { value: CLit::Int { value: 5 } }),
1075 body: Box::new(inner),
1076 };
1077 let stage = Stage::FnDecl(FnDecl {
1078 name: "g".into(),
1079 type_params: Vec::new(),
1080 params: vec![Param {
1081 name: "n".into(),
1082 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1083 }],
1084 effects: Vec::new(),
1085 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1086 body,
1087 examples: Vec::new(),
1088 });
1089 let out = inline_let(&stage, &NodeId("n_0.2".into())).unwrap();
1090 let Stage::FnDecl(fd) = out else { panic!() };
1091 let CExpr::Let { name, .. } = fd.body else { panic!() };
1093 assert_eq!(name, "x", "inner let preserved");
1094 }
1095
1096 #[test]
1097 fn inline_let_not_a_let_target_errors() {
1098 let stage = match_stage_with_two_arms();
1099 let err = inline_let(&stage, &NodeId("n_0.2".into())).unwrap_err();
1100 assert!(matches!(err, TransformError::NotALet { found_kind: "Match", .. }));
1101 }
1102
1103 fn extract_stage() -> Stage {
1108 let body = CExpr::BinOp {
1109 op: "+".into(),
1110 lhs: Box::new(CExpr::BinOp {
1111 op: "*".into(),
1112 lhs: Box::new(CExpr::Var { name: "n".into() }),
1113 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
1114 }),
1115 rhs: Box::new(CExpr::Var { name: "m".into() }),
1116 };
1117 Stage::FnDecl(FnDecl {
1118 name: "caller".into(),
1119 type_params: Vec::new(),
1120 params: vec![
1121 Param {
1122 name: "n".into(),
1123 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1124 },
1125 Param {
1126 name: "m".into(),
1127 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1128 },
1129 ],
1130 effects: Vec::new(),
1131 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1132 body,
1133 examples: Vec::new(),
1134 })
1135 }
1136
1137 fn double_n_spec() -> ExtractFnSpec {
1138 ExtractFnSpec {
1139 name: "double_n".into(),
1140 type_params: Vec::new(),
1141 params: vec![Param {
1142 name: "n".into(),
1143 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1144 }],
1145 effects: Vec::new(),
1146 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1147 }
1148 }
1149
1150 #[test]
1151 fn extract_function_replaces_subexpression_with_call() {
1152 let stage = extract_stage();
1156 let (modified, new_fn) = extract_function(
1157 &stage,
1158 &NodeId("n_0.3.0".into()),
1159 double_n_spec(),
1160 ).unwrap();
1161
1162 let Stage::FnDecl(fd) = modified else { panic!() };
1164 let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
1165 let CExpr::Call { callee, args } = *lhs else { panic!() };
1166 assert!(matches!(*callee, CExpr::Var { name: ref n } if n == "double_n"));
1167 assert_eq!(args.len(), 1);
1168 assert!(matches!(args[0], CExpr::Var { name: ref n } if n == "n"));
1169
1170 let Stage::FnDecl(new_fd) = new_fn else { panic!() };
1172 assert_eq!(new_fd.name, "double_n");
1173 assert_eq!(new_fd.params.len(), 1);
1174 assert_eq!(new_fd.params[0].name, "n");
1175 let CExpr::BinOp { op, lhs, rhs, .. } = new_fd.body else { panic!() };
1177 assert_eq!(op, "*");
1178 assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "n"));
1179 assert!(matches!(*rhs, CExpr::Literal { value: CLit::Int { value: 2 } }));
1180 }
1181
1182 #[test]
1183 fn extract_function_refuses_extra_params() {
1184 let stage = extract_stage();
1186 let mut spec = double_n_spec();
1187 spec.params.push(Param {
1188 name: "z".into(),
1189 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1190 });
1191 let err = extract_function(&stage, &NodeId("n_0.3.0".into()), spec).unwrap_err();
1192 assert!(matches!(err, TransformError::ExtractFnRefused { .. }), "got {err:?}");
1193 }
1194
1195 #[test]
1196 fn extract_function_refuses_missing_params() {
1197 let stage = extract_stage();
1199 let spec = ExtractFnSpec {
1200 name: "no_args".into(),
1201 type_params: Vec::new(),
1202 params: Vec::new(),
1203 effects: Vec::new(),
1204 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1205 };
1206 let err = extract_function(&stage, &NodeId("n_0.3.0".into()), spec).unwrap_err();
1207 assert!(matches!(err, TransformError::ExtractFnRefused { .. }), "got {err:?}");
1208 }
1209
1210 #[test]
1211 fn extract_function_handles_zero_free_vars() {
1212 let body = CExpr::BinOp {
1214 op: "+".into(),
1215 lhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
1216 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
1217 };
1218 let stage = Stage::FnDecl(FnDecl {
1219 name: "caller".into(),
1220 type_params: Vec::new(),
1221 params: Vec::new(),
1222 effects: Vec::new(),
1223 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1224 body,
1225 examples: Vec::new(),
1226 });
1227 let spec = ExtractFnSpec {
1228 name: "one".into(),
1229 type_params: Vec::new(),
1230 params: Vec::new(),
1231 effects: Vec::new(),
1232 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1233 };
1234 let (modified, new_fn) = extract_function(
1236 &stage, &NodeId("n_0.1.0".into()), spec,
1237 ).unwrap();
1238 let Stage::FnDecl(fd) = modified else { panic!() };
1239 let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
1240 let CExpr::Call { args, .. } = *lhs else { panic!() };
1241 assert_eq!(args.len(), 0, "no args for zero-free-var extract");
1242 let Stage::FnDecl(new_fd) = new_fn else { panic!() };
1243 assert!(matches!(new_fd.body, CExpr::Literal { value: CLit::Int { value: 1 } }));
1244 }
1245
1246 #[test]
1247 fn extract_function_typedecl_target_errors() {
1248 let stage = Stage::TypeDecl(crate::canonical::TypeDecl {
1249 name: "T".into(),
1250 params: Vec::new(),
1251 definition: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1252 });
1253 let err = extract_function(&stage, &NodeId("n_0.0".into()), double_n_spec())
1254 .unwrap_err();
1255 assert!(matches!(err, TransformError::NonFnTarget { stage_kind: "TypeDecl" }));
1256 }
1257
1258 fn match_stage_with_two_arms() -> Stage {
1261 let body = CExpr::Match {
1264 scrutinee: Box::new(CExpr::Var { name: "n".into() }),
1265 arms: vec![
1266 Arm {
1267 pattern: Pattern::PLiteral { value: CLit::Int { value: 0 } },
1268 body: CExpr::Literal { value: CLit::Int { value: 1 } },
1269 },
1270 Arm {
1271 pattern: Pattern::PWild,
1272 body: CExpr::Literal { value: CLit::Int { value: 2 } },
1273 },
1274 ],
1275 };
1276 Stage::FnDecl(FnDecl {
1277 name: "pick".into(),
1278 type_params: Vec::new(),
1279 params: vec![Param {
1280 name: "n".into(),
1281 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1282 }],
1283 effects: Vec::new(),
1284 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1285 body,
1286 examples: Vec::new(),
1287 })
1288 }
1289
1290 fn match_node_id() -> NodeId {
1291 NodeId("n_0.2".into())
1295 }
1296
1297 #[test]
1298 fn replace_first_arm_body_succeeds() {
1299 let stage = match_stage_with_two_arms();
1300 let new_body = CExpr::Literal { value: CLit::Int { value: 42 } };
1301 let out = replace_match_arm(&stage, &match_node_id(), 0, new_body).unwrap();
1302 let Stage::FnDecl(fd) = out else { panic!() };
1303 let CExpr::Match { arms, .. } = fd.body else { panic!() };
1304 assert_eq!(arms.len(), 2);
1305 assert!(matches!(arms[0].body, CExpr::Literal { value: CLit::Int { value: 42 } }));
1306 assert!(matches!(arms[1].body, CExpr::Literal { value: CLit::Int { value: 2 } }));
1308 assert!(matches!(arms[0].pattern, Pattern::PLiteral { .. }));
1310 }
1311
1312 #[test]
1313 fn replace_second_arm_preserves_first() {
1314 let stage = match_stage_with_two_arms();
1315 let new_body = CExpr::Literal { value: CLit::Int { value: 99 } };
1316 let out = replace_match_arm(&stage, &match_node_id(), 1, new_body).unwrap();
1317 let Stage::FnDecl(fd) = out else { panic!() };
1318 let CExpr::Match { arms, .. } = fd.body else { panic!() };
1319 assert!(matches!(arms[0].body, CExpr::Literal { value: CLit::Int { value: 1 } }));
1320 assert!(matches!(arms[1].body, CExpr::Literal { value: CLit::Int { value: 99 } }));
1321 }
1322
1323 #[test]
1324 fn arm_index_out_of_range_errors() {
1325 let stage = match_stage_with_two_arms();
1326 let new_body = CExpr::Literal { value: CLit::Unit };
1327 let err = replace_match_arm(&stage, &match_node_id(), 5, new_body).unwrap_err();
1328 assert!(matches!(err, TransformError::ArmIndexOutOfRange { arm_count: 2, requested: 5, .. }));
1329 }
1330
1331 #[test]
1332 fn non_match_target_errors() {
1333 let stage = match_stage_with_two_arms();
1335 let new_body = CExpr::Literal { value: CLit::Unit };
1336 let err = replace_match_arm(&stage, &NodeId("n_0.2.0".into()), 0, new_body)
1337 .unwrap_err();
1338 assert!(matches!(err, TransformError::NotAMatch { found_kind: "Var", .. }),
1339 "got {err:?}");
1340 }
1341
1342 #[test]
1343 fn unknown_node_errors() {
1344 let stage = match_stage_with_two_arms();
1345 let new_body = CExpr::Literal { value: CLit::Unit };
1346 let err = replace_match_arm(&stage, &NodeId("n_0.99".into()), 0, new_body)
1347 .unwrap_err();
1348 assert!(matches!(err, TransformError::UnknownNode { .. }), "got {err:?}");
1349 }
1350
1351 #[test]
1352 fn typedecl_target_errors() {
1353 let stage = Stage::TypeDecl(crate::canonical::TypeDecl {
1354 name: "T".into(),
1355 params: Vec::new(),
1356 definition: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1357 });
1358 let err = replace_match_arm(&stage, &match_node_id(), 0,
1359 CExpr::Literal { value: CLit::Unit }).unwrap_err();
1360 assert!(matches!(err, TransformError::NonFnTarget { stage_kind: "TypeDecl" }));
1361 }
1362}