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 effect_row_var: None,
651 return_type: spec.return_type,
652 body: extracted_expr,
653 examples: Vec::new(),
654 });
655
656 Ok((modified, new_fn))
657}
658
659fn parse_node_id(id: &str) -> Result<Vec<usize>, TransformError> {
660 let s = id.strip_prefix("n_").ok_or_else(|| TransformError::BadNodeId(id.into()))?;
661 let mut parts = s.split('.');
662 let head = parts.next().ok_or_else(|| TransformError::BadNodeId(id.into()))?;
663 if head != "0" {
664 return Err(TransformError::BadNodeId(id.into()));
665 }
666 let mut out = Vec::new();
667 for p in parts {
668 out.push(p.parse::<usize>().map_err(|_| TransformError::BadNodeId(id.into()))?);
669 }
670 Ok(out)
671}
672
673fn navigate_to_expr<'a>(
679 root: &'a mut CExpr,
680 path: &[usize],
681 target_id: &str,
682) -> Result<&'a mut CExpr, TransformError> {
683 let mut current = root;
684 for &idx in path {
685 current = step_expr(current, idx)
686 .ok_or_else(|| TransformError::UnknownNode { at: target_id.into() })?;
687 }
688 Ok(current)
689}
690
691fn step_expr(e: &mut CExpr, idx: usize) -> Option<&mut CExpr> {
695 match e {
696 CExpr::Call { callee, args } => {
697 if idx == 0 { return Some(callee); }
698 args.get_mut(idx - 1)
699 }
700 CExpr::Let { value, body, .. } => {
701 match idx {
702 0 => Some(value),
703 1 => Some(body),
704 _ => None,
705 }
706 }
707 CExpr::Match { scrutinee, arms } => {
708 if idx == 0 { return Some(scrutinee); }
709 let arm_off = idx - 1;
714 if arm_off % 2 != 1 {
715 return None;
716 }
717 let arm_index = arm_off / 2;
718 arms.get_mut(arm_index).map(|a| &mut a.body)
719 }
720 CExpr::Block { statements, result } => {
721 if idx < statements.len() {
722 statements.get_mut(idx)
723 } else if idx == statements.len() {
724 Some(result)
725 } else {
726 None
727 }
728 }
729 CExpr::Constructor { args, .. } | CExpr::TupleLit { items: args, .. }
730 | CExpr::ListLit { items: args, .. } => args.get_mut(idx),
731 CExpr::RecordLit { fields } => fields.get_mut(idx).map(|f| &mut f.value),
732 CExpr::FieldAccess { value, .. } => if idx == 0 { Some(value) } else { None },
733 CExpr::Lambda { body, .. } => if idx == 0 { Some(body) } else { None },
734 CExpr::BinOp { lhs, rhs, .. } => match idx {
735 0 => Some(lhs), 1 => Some(rhs), _ => None,
736 },
737 CExpr::UnaryOp { expr, .. } => if idx == 0 { Some(expr) } else { None },
738 CExpr::Return { value } => if idx == 0 { Some(value) } else { None },
739 _ => None,
740 }
741}
742
743fn cexpr_kind(e: &CExpr) -> &'static str {
744 match e {
745 CExpr::Literal { .. } => "Literal",
746 CExpr::Var { .. } => "Var",
747 CExpr::Call { .. } => "Call",
748 CExpr::Let { .. } => "Let",
749 CExpr::Match { .. } => "Match",
750 CExpr::Block { .. } => "Block",
751 CExpr::Constructor { .. } => "Constructor",
752 CExpr::RecordLit { .. } => "RecordLit",
753 CExpr::TupleLit { .. } => "TupleLit",
754 CExpr::ListLit { .. } => "ListLit",
755 CExpr::FieldAccess { .. } => "FieldAccess",
756 CExpr::Lambda { .. } => "Lambda",
757 CExpr::BinOp { .. } => "BinOp",
758 CExpr::UnaryOp { .. } => "UnaryOp",
759 CExpr::Return { .. } => "Return",
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use crate::canonical::{Arm, CLit, FnDecl, Param, Pattern, TypeExpr};
767
768 fn let_stage() -> Stage {
772 let body = CExpr::Let {
773 name: "x".into(),
774 ty: None,
775 value: Box::new(CExpr::BinOp {
776 op: "+".into(),
777 lhs: Box::new(CExpr::Var { name: "n".into() }),
778 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
779 }),
780 body: Box::new(CExpr::BinOp {
781 op: "+".into(),
782 lhs: Box::new(CExpr::Var { name: "x".into() }),
783 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
784 }),
785 };
786 Stage::FnDecl(FnDecl {
787 name: "outer".into(),
788 type_params: Vec::new(),
789 params: vec![Param {
790 name: "n".into(),
791 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
792 }],
793 effects: Vec::new(),
794 effect_row_var: None,
795 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
796 body,
797 examples: Vec::new(),
798 })
799 }
800
801 fn let_node_id() -> NodeId { NodeId("n_0.2".into()) }
802
803 #[test]
804 fn rename_local_renames_binding_and_body_reference() {
805 let stage = let_stage();
806 let out = rename_local(&stage, &let_node_id(), "y").unwrap();
807 let Stage::FnDecl(fd) = out else { panic!() };
808 let CExpr::Let { name, value, body, .. } = fd.body else { panic!() };
809 assert_eq!(name, "y", "binding renamed");
810 let CExpr::BinOp { lhs, .. } = *value else { panic!() };
812 assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "n"));
813 let CExpr::BinOp { lhs, .. } = *body else { panic!() };
815 assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "y"));
816 }
817
818 #[test]
819 fn rename_local_refuses_no_op() {
820 let stage = let_stage();
821 let err = rename_local(&stage, &let_node_id(), "x").unwrap_err();
822 assert!(matches!(err, TransformError::RenameNoOp { .. }));
823 }
824
825 #[test]
826 fn rename_local_respects_inner_let_shadowing() {
827 let inner = CExpr::Let {
831 name: "x".into(),
832 ty: None,
833 value: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
834 body: Box::new(CExpr::Var { name: "x".into() }),
835 };
836 let body = CExpr::Let {
837 name: "x".into(),
838 ty: None,
839 value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
840 body: Box::new(inner),
841 };
842 let stage = Stage::FnDecl(FnDecl {
843 name: "f".into(),
844 type_params: Vec::new(),
845 params: Vec::new(),
846 effects: Vec::new(),
847 effect_row_var: None,
848 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
849 body,
850 examples: Vec::new(),
851 });
852 let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
853 let Stage::FnDecl(fd) = out else { panic!() };
854 let CExpr::Let { name: outer_name, body: outer_body, .. } = fd.body else { panic!() };
855 assert_eq!(outer_name, "y", "outer let renamed");
856 let CExpr::Let { name: inner_name, body: inner_body, .. } = *outer_body else { panic!() };
857 assert_eq!(inner_name, "x");
859 assert!(matches!(*inner_body, CExpr::Var { name: ref n } if n == "x"));
861 }
862
863 #[test]
864 fn rename_local_respects_lambda_param_shadowing() {
865 let lambda = CExpr::Lambda {
867 params: vec![Param {
868 name: "x".into(),
869 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
870 }],
871 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
872 effects: Vec::new(),
873 effect_row_var: None,
874 body: Box::new(CExpr::Var { name: "x".into() }),
875 };
876 let body = CExpr::Let {
877 name: "x".into(),
878 ty: None,
879 value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
880 body: Box::new(lambda),
881 };
882 let stage = Stage::FnDecl(FnDecl {
883 name: "f".into(),
884 type_params: Vec::new(),
885 params: Vec::new(),
886 effects: Vec::new(),
887 effect_row_var: None,
888 return_type: TypeExpr::Function {
889 params: vec![TypeExpr::Named { name: "Int".into(), args: Vec::new() }],
890 effects: Vec::new(),
891 effect_row_var: None,
892 ret: Box::new(TypeExpr::Named { name: "Int".into(), args: Vec::new() }),
893 },
894 body,
895 examples: Vec::new(),
896 });
897 let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
898 let Stage::FnDecl(fd) = out else { panic!() };
899 let CExpr::Let { name, body: outer_body, .. } = fd.body else { panic!() };
900 assert_eq!(name, "y");
901 let CExpr::Lambda { body: lam_body, .. } = *outer_body else { panic!() };
902 assert!(matches!(*lam_body, CExpr::Var { name: ref n } if n == "x"));
904 }
905
906 #[test]
907 fn rename_local_respects_match_pattern_shadowing() {
908 let match_expr = CExpr::Match {
915 scrutinee: Box::new(CExpr::Var { name: "foo".into() }),
916 arms: vec![
917 Arm {
918 pattern: Pattern::PVar { name: "x".into() },
919 body: CExpr::Var { name: "x".into() },
920 },
921 Arm {
922 pattern: Pattern::PWild,
923 body: CExpr::Var { name: "x".into() },
924 },
925 ],
926 };
927 let body = CExpr::Let {
928 name: "x".into(),
929 ty: None,
930 value: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
931 body: Box::new(match_expr),
932 };
933 let stage = Stage::FnDecl(FnDecl {
934 name: "f".into(),
935 type_params: Vec::new(),
936 params: Vec::new(),
937 effects: Vec::new(),
938 effect_row_var: None,
939 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
940 body,
941 examples: Vec::new(),
942 });
943 let out = rename_local(&stage, &NodeId("n_0.1".into()), "y").unwrap();
944 let Stage::FnDecl(fd) = out else { panic!() };
945 let CExpr::Let { body: outer_body, .. } = fd.body else { panic!() };
946 let CExpr::Match { arms, .. } = *outer_body else { panic!() };
947 assert!(matches!(arms[0].body, CExpr::Var { name: ref n } if n == "x"));
949 assert!(matches!(arms[1].body, CExpr::Var { name: ref n } if n == "y"));
951 }
952
953 #[test]
954 fn rename_local_not_a_let_errors() {
955 let stage = match_stage_with_two_arms();
958 let err = rename_local(&stage, &NodeId("n_0.2".into()), "y").unwrap_err();
959 assert!(matches!(err, TransformError::NotALet { found_kind: "Match", .. }),
960 "got {err:?}");
961 }
962
963 fn inlinable_stage() -> Stage {
967 let body = CExpr::Let {
968 name: "x".into(),
969 ty: None,
970 value: Box::new(CExpr::Literal { value: CLit::Int { value: 5 } }),
971 body: Box::new(CExpr::BinOp {
972 op: "+".into(),
973 lhs: Box::new(CExpr::Var { name: "x".into() }),
974 rhs: Box::new(CExpr::Var { name: "n".into() }),
975 }),
976 };
977 Stage::FnDecl(FnDecl {
978 name: "f".into(),
979 type_params: Vec::new(),
980 params: vec![Param {
981 name: "n".into(),
982 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
983 }],
984 effects: Vec::new(),
985 effect_row_var: None,
986 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
987 body,
988 examples: Vec::new(),
989 })
990 }
991
992 #[test]
993 fn inline_let_substitutes_literal_value() {
994 let stage = inlinable_stage();
995 let out = inline_let(&stage, &NodeId("n_0.2".into())).unwrap();
996 let Stage::FnDecl(fd) = out else { panic!() };
997 let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
999 assert!(matches!(*lhs, CExpr::Literal { value: CLit::Int { value: 5 } }));
1000 }
1001
1002 #[test]
1003 fn inline_let_refuses_call_in_value() {
1004 let body = CExpr::Let {
1006 name: "x".into(),
1007 ty: None,
1008 value: Box::new(CExpr::Call {
1009 callee: Box::new(CExpr::Var { name: "f".into() }),
1010 args: Vec::new(),
1011 }),
1012 body: Box::new(CExpr::Var { name: "x".into() }),
1013 };
1014 let stage = Stage::FnDecl(FnDecl {
1015 name: "g".into(),
1016 type_params: Vec::new(),
1017 params: Vec::new(),
1018 effects: Vec::new(),
1019 effect_row_var: None,
1020 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1021 body,
1022 examples: Vec::new(),
1023 });
1024 let err = inline_let(&stage, &NodeId("n_0.1".into())).unwrap_err();
1025 assert!(matches!(err, TransformError::InlineLetRefused { .. }), "got {err:?}");
1026 }
1027
1028 #[test]
1029 fn inline_let_refuses_capture() {
1030 let inner = CExpr::Let {
1033 name: "y".into(),
1034 ty: None,
1035 value: Box::new(CExpr::Literal { value: CLit::Int { value: 7 } }),
1036 body: Box::new(CExpr::BinOp {
1037 op: "+".into(),
1038 lhs: Box::new(CExpr::Var { name: "x".into() }),
1039 rhs: Box::new(CExpr::Var { name: "y".into() }),
1040 }),
1041 };
1042 let body = CExpr::Let {
1043 name: "x".into(),
1044 ty: None,
1045 value: Box::new(CExpr::Var { name: "y".into() }),
1046 body: Box::new(inner),
1047 };
1048 let stage = Stage::FnDecl(FnDecl {
1049 name: "g".into(),
1050 type_params: Vec::new(),
1051 params: vec![Param {
1052 name: "y".into(),
1053 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1054 }],
1055 effects: Vec::new(),
1056 effect_row_var: None,
1057 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1058 body,
1059 examples: Vec::new(),
1060 });
1061 let err = inline_let(&stage, &NodeId("n_0.2".into())).unwrap_err();
1063 assert!(matches!(err, TransformError::InlineLetRefused { .. }), "got {err:?}");
1064 }
1065
1066 #[test]
1067 fn inline_let_substitutes_under_shadowing() {
1068 let inner = CExpr::Let {
1073 name: "x".into(),
1074 ty: None,
1075 value: Box::new(CExpr::Var { name: "n".into() }),
1076 body: Box::new(CExpr::BinOp {
1077 op: "+".into(),
1078 lhs: Box::new(CExpr::Var { name: "x".into() }),
1079 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
1080 }),
1081 };
1082 let body = CExpr::Let {
1083 name: "x".into(),
1084 ty: None,
1085 value: Box::new(CExpr::Literal { value: CLit::Int { value: 5 } }),
1086 body: Box::new(inner),
1087 };
1088 let stage = Stage::FnDecl(FnDecl {
1089 name: "g".into(),
1090 type_params: Vec::new(),
1091 params: vec![Param {
1092 name: "n".into(),
1093 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1094 }],
1095 effects: Vec::new(),
1096 effect_row_var: None,
1097 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1098 body,
1099 examples: Vec::new(),
1100 });
1101 let out = inline_let(&stage, &NodeId("n_0.2".into())).unwrap();
1102 let Stage::FnDecl(fd) = out else { panic!() };
1103 let CExpr::Let { name, .. } = fd.body else { panic!() };
1105 assert_eq!(name, "x", "inner let preserved");
1106 }
1107
1108 #[test]
1109 fn inline_let_not_a_let_target_errors() {
1110 let stage = match_stage_with_two_arms();
1111 let err = inline_let(&stage, &NodeId("n_0.2".into())).unwrap_err();
1112 assert!(matches!(err, TransformError::NotALet { found_kind: "Match", .. }));
1113 }
1114
1115 fn extract_stage() -> Stage {
1120 let body = CExpr::BinOp {
1121 op: "+".into(),
1122 lhs: Box::new(CExpr::BinOp {
1123 op: "*".into(),
1124 lhs: Box::new(CExpr::Var { name: "n".into() }),
1125 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
1126 }),
1127 rhs: Box::new(CExpr::Var { name: "m".into() }),
1128 };
1129 Stage::FnDecl(FnDecl {
1130 name: "caller".into(),
1131 type_params: Vec::new(),
1132 params: vec![
1133 Param {
1134 name: "n".into(),
1135 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1136 },
1137 Param {
1138 name: "m".into(),
1139 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1140 },
1141 ],
1142 effects: Vec::new(),
1143 effect_row_var: None,
1144 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1145 body,
1146 examples: Vec::new(),
1147 })
1148 }
1149
1150 fn double_n_spec() -> ExtractFnSpec {
1151 ExtractFnSpec {
1152 name: "double_n".into(),
1153 type_params: Vec::new(),
1154 params: vec![Param {
1155 name: "n".into(),
1156 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1157 }],
1158 effects: Vec::new(),
1159 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1160 }
1161 }
1162
1163 #[test]
1164 fn extract_function_replaces_subexpression_with_call() {
1165 let stage = extract_stage();
1169 let (modified, new_fn) = extract_function(
1170 &stage,
1171 &NodeId("n_0.3.0".into()),
1172 double_n_spec(),
1173 ).unwrap();
1174
1175 let Stage::FnDecl(fd) = modified else { panic!() };
1177 let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
1178 let CExpr::Call { callee, args } = *lhs else { panic!() };
1179 assert!(matches!(*callee, CExpr::Var { name: ref n } if n == "double_n"));
1180 assert_eq!(args.len(), 1);
1181 assert!(matches!(args[0], CExpr::Var { name: ref n } if n == "n"));
1182
1183 let Stage::FnDecl(new_fd) = new_fn else { panic!() };
1185 assert_eq!(new_fd.name, "double_n");
1186 assert_eq!(new_fd.params.len(), 1);
1187 assert_eq!(new_fd.params[0].name, "n");
1188 let CExpr::BinOp { op, lhs, rhs, .. } = new_fd.body else { panic!() };
1190 assert_eq!(op, "*");
1191 assert!(matches!(*lhs, CExpr::Var { name: ref n } if n == "n"));
1192 assert!(matches!(*rhs, CExpr::Literal { value: CLit::Int { value: 2 } }));
1193 }
1194
1195 #[test]
1196 fn extract_function_refuses_extra_params() {
1197 let stage = extract_stage();
1199 let mut spec = double_n_spec();
1200 spec.params.push(Param {
1201 name: "z".into(),
1202 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1203 });
1204 let err = extract_function(&stage, &NodeId("n_0.3.0".into()), spec).unwrap_err();
1205 assert!(matches!(err, TransformError::ExtractFnRefused { .. }), "got {err:?}");
1206 }
1207
1208 #[test]
1209 fn extract_function_refuses_missing_params() {
1210 let stage = extract_stage();
1212 let spec = ExtractFnSpec {
1213 name: "no_args".into(),
1214 type_params: Vec::new(),
1215 params: Vec::new(),
1216 effects: Vec::new(),
1217 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1218 };
1219 let err = extract_function(&stage, &NodeId("n_0.3.0".into()), spec).unwrap_err();
1220 assert!(matches!(err, TransformError::ExtractFnRefused { .. }), "got {err:?}");
1221 }
1222
1223 #[test]
1224 fn extract_function_handles_zero_free_vars() {
1225 let body = CExpr::BinOp {
1227 op: "+".into(),
1228 lhs: Box::new(CExpr::Literal { value: CLit::Int { value: 1 } }),
1229 rhs: Box::new(CExpr::Literal { value: CLit::Int { value: 2 } }),
1230 };
1231 let stage = Stage::FnDecl(FnDecl {
1232 name: "caller".into(),
1233 type_params: Vec::new(),
1234 params: Vec::new(),
1235 effects: Vec::new(),
1236 effect_row_var: None,
1237 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1238 body,
1239 examples: Vec::new(),
1240 });
1241 let spec = ExtractFnSpec {
1242 name: "one".into(),
1243 type_params: Vec::new(),
1244 params: Vec::new(),
1245 effects: Vec::new(),
1246 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1247 };
1248 let (modified, new_fn) = extract_function(
1250 &stage, &NodeId("n_0.1.0".into()), spec,
1251 ).unwrap();
1252 let Stage::FnDecl(fd) = modified else { panic!() };
1253 let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
1254 let CExpr::Call { args, .. } = *lhs else { panic!() };
1255 assert_eq!(args.len(), 0, "no args for zero-free-var extract");
1256 let Stage::FnDecl(new_fd) = new_fn else { panic!() };
1257 assert!(matches!(new_fd.body, CExpr::Literal { value: CLit::Int { value: 1 } }));
1258 }
1259
1260 #[test]
1261 fn extract_function_typedecl_target_errors() {
1262 let stage = Stage::TypeDecl(crate::canonical::TypeDecl {
1263 name: "T".into(),
1264 params: Vec::new(),
1265 definition: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1266 });
1267 let err = extract_function(&stage, &NodeId("n_0.0".into()), double_n_spec())
1268 .unwrap_err();
1269 assert!(matches!(err, TransformError::NonFnTarget { stage_kind: "TypeDecl" }));
1270 }
1271
1272 fn match_stage_with_two_arms() -> Stage {
1275 let body = CExpr::Match {
1278 scrutinee: Box::new(CExpr::Var { name: "n".into() }),
1279 arms: vec![
1280 Arm {
1281 pattern: Pattern::PLiteral { value: CLit::Int { value: 0 } },
1282 body: CExpr::Literal { value: CLit::Int { value: 1 } },
1283 },
1284 Arm {
1285 pattern: Pattern::PWild,
1286 body: CExpr::Literal { value: CLit::Int { value: 2 } },
1287 },
1288 ],
1289 };
1290 Stage::FnDecl(FnDecl {
1291 name: "pick".into(),
1292 type_params: Vec::new(),
1293 params: vec![Param {
1294 name: "n".into(),
1295 ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1296 }],
1297 effects: Vec::new(),
1298 effect_row_var: None,
1299 return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1300 body,
1301 examples: Vec::new(),
1302 })
1303 }
1304
1305 fn match_node_id() -> NodeId {
1306 NodeId("n_0.2".into())
1310 }
1311
1312 #[test]
1313 fn replace_first_arm_body_succeeds() {
1314 let stage = match_stage_with_two_arms();
1315 let new_body = CExpr::Literal { value: CLit::Int { value: 42 } };
1316 let out = replace_match_arm(&stage, &match_node_id(), 0, new_body).unwrap();
1317 let Stage::FnDecl(fd) = out else { panic!() };
1318 let CExpr::Match { arms, .. } = fd.body else { panic!() };
1319 assert_eq!(arms.len(), 2);
1320 assert!(matches!(arms[0].body, CExpr::Literal { value: CLit::Int { value: 42 } }));
1321 assert!(matches!(arms[1].body, CExpr::Literal { value: CLit::Int { value: 2 } }));
1323 assert!(matches!(arms[0].pattern, Pattern::PLiteral { .. }));
1325 }
1326
1327 #[test]
1328 fn replace_second_arm_preserves_first() {
1329 let stage = match_stage_with_two_arms();
1330 let new_body = CExpr::Literal { value: CLit::Int { value: 99 } };
1331 let out = replace_match_arm(&stage, &match_node_id(), 1, new_body).unwrap();
1332 let Stage::FnDecl(fd) = out else { panic!() };
1333 let CExpr::Match { arms, .. } = fd.body else { panic!() };
1334 assert!(matches!(arms[0].body, CExpr::Literal { value: CLit::Int { value: 1 } }));
1335 assert!(matches!(arms[1].body, CExpr::Literal { value: CLit::Int { value: 99 } }));
1336 }
1337
1338 #[test]
1339 fn arm_index_out_of_range_errors() {
1340 let stage = match_stage_with_two_arms();
1341 let new_body = CExpr::Literal { value: CLit::Unit };
1342 let err = replace_match_arm(&stage, &match_node_id(), 5, new_body).unwrap_err();
1343 assert!(matches!(err, TransformError::ArmIndexOutOfRange { arm_count: 2, requested: 5, .. }));
1344 }
1345
1346 #[test]
1347 fn non_match_target_errors() {
1348 let stage = match_stage_with_two_arms();
1350 let new_body = CExpr::Literal { value: CLit::Unit };
1351 let err = replace_match_arm(&stage, &NodeId("n_0.2.0".into()), 0, new_body)
1352 .unwrap_err();
1353 assert!(matches!(err, TransformError::NotAMatch { found_kind: "Var", .. }),
1354 "got {err:?}");
1355 }
1356
1357 #[test]
1358 fn unknown_node_errors() {
1359 let stage = match_stage_with_two_arms();
1360 let new_body = CExpr::Literal { value: CLit::Unit };
1361 let err = replace_match_arm(&stage, &NodeId("n_0.99".into()), 0, new_body)
1362 .unwrap_err();
1363 assert!(matches!(err, TransformError::UnknownNode { .. }), "got {err:?}");
1364 }
1365
1366 #[test]
1367 fn typedecl_target_errors() {
1368 let stage = Stage::TypeDecl(crate::canonical::TypeDecl {
1369 name: "T".into(),
1370 params: Vec::new(),
1371 definition: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
1372 });
1373 let err = replace_match_arm(&stage, &match_node_id(), 0,
1374 CExpr::Literal { value: CLit::Unit }).unwrap_err();
1375 assert!(matches!(err, TransformError::NonFnTarget { stage_kind: "TypeDecl" }));
1376 }
1377}