API migration transducers provide WFST-based automation for transforming code between API versions. This document explains how to define migration rules and apply them to modernize codebases.
API migration is the process of updating code to work with a newer version of a library, framework, or language. Common scenarios include:
- Function renames:
oldFunction()→newFunction() - Parameter changes: New required parameters, renamed parameters
- Signature changes: Different argument types or orders
- Deprecations: Removal of old APIs with replacement suggestions
- Syntax evolution: Python 2 to Python 3, ES5 to ES6
Traditional approaches (regex, AST rewriting) have limitations:
| Approach | Strengths | Weaknesses |
|---|---|---|
| Regex | Fast, simple | No context, fragile |
| AST rewriting | Precise, structural | Language-specific, complex |
| WFST | Context-aware, composable, weighted | Token-based |
WFSTs excel at:
- Pattern matching with context
- Weighted alternatives when multiple migrations are possible
- Composition of migration passes
- Incremental application through streaming
┌─────────────────────────────────────────────────────┐
│ API Migration System │
├─────────────────────────────────────────────────────┤
│ Migration Rules │
│ ├── RenameFunction("old" → "new") │
│ ├── RenameType("OldType" → "NewType") │
│ ├── ReplaceCall(pattern → replacement) │
│ └── Custom transformations │
├─────────────────────────────────────────────────────┤
│ Version Range │
│ └── from: v1.0.0 to: v2.0.0 │
├─────────────────────────────────────────────────────┤
│ Migration Transducer │
│ ├── Token-based pattern matching │
│ ├── Rule application with costs │
│ └── WFST compilation for composition │
└─────────────────────────────────────────────────────┘
Semantic versioning support for constraining rule applicability:
use lling_llang::programming::{Version, VersionRange};
// Create versions
let v1_0 = Version::new(1, 0); // 1.0.0
let v2_0 = Version::new(2, 0); // 2.0.0
let v1_2_3 = Version::with_patch(1, 2, 3); // 1.2.3
// Parse from strings
let parsed = Version::parse("1.5.2").unwrap();
// Version ordering
assert!(v1_0 < v2_0);
assert!(v1_0 < v1_2_3);
// Version ranges
let range = VersionRange::new(v1_0, v2_0);
assert!(range.contains(v1_2_3)); // 1.0.0 ≤ 1.2.3 ≤ 2.0.0
assert!(!range.contains(Version::new(3, 0)));Different kinds of API changes:
use lling_llang::programming::MigrationType;
// Function rename
MigrationType::RenameFunction {
old_name: "deprecated_fn".to_string(),
new_name: "modern_fn".to_string(),
}
// Parameter rename (optionally scoped to a function)
MigrationType::RenameParameter {
function: Some("my_function".to_string()),
old_name: "old_param".to_string(),
new_name: "new_param".to_string(),
}
// Type rename
MigrationType::RenameType {
old_name: "OldClass".to_string(),
new_name: "NewClass".to_string(),
}
// Multi-token pattern replacement
MigrationType::ReplaceCall {
old_pattern: vec!["obj".to_string(), ".", "method".to_string()],
new_pattern: vec!["obj".to_string(), ".", "newMethod".to_string()],
}
// Deprecation with message
MigrationType::RemoveFunction {
function: "legacy_fn".to_string(),
message: "Use modern_fn instead".to_string(),
}
// Custom transformation
MigrationType::Custom {
description: "Custom transformation".to_string(),
old_tokens: vec!["old".to_string(), "pattern".to_string()],
new_tokens: vec!["new".to_string(), "pattern".to_string()],
}Rules define individual transformations:
use lling_llang::programming::{ApiMigrationRule, Version};
// Function rename rule
let rename_rule = ApiMigrationRule::rename_function(
"getUser",
"fetchUser",
Version::new(1, 0),
Version::new(2, 0),
);
// Type rename rule
let type_rule = ApiMigrationRule::rename_type(
"Response",
"ApiResponse",
Version::new(1, 0),
Version::new(2, 0),
);
// Parameter rename (function-scoped)
let param_rule = ApiMigrationRule::rename_parameter(
Some("configure"), // Only in configure() calls
"timeout",
"timeoutMs",
Version::new(1, 0),
Version::new(2, 0),
);
// Multi-token pattern replacement
let pattern_rule = ApiMigrationRule::replace(
["this", ".", "setState"], // Old pattern
["setState"], // New pattern
Version::new(16, 8),
Version::new(18, 0),
);
// Deprecation warning (requires manual review)
let deprecation = ApiMigrationRule::deprecate(
"unsafeMethod",
"This method is deprecated with no replacement",
Version::new(1, 0),
Version::new(2, 0),
);
// Customize rule cost and review requirement
let custom_rule = ApiMigrationRule::rename_function("a", "b", Version::new(1, 0), Version::new(2, 0))
.with_cost(0.5) // Higher cost = less preferred
.manual_review(); // Mark as needing human reviewThe transducer applies rules to token streams:
use lling_llang::programming::{ApiMigrationTransducer, ApiMigrationRule, Version};
use lling_llang::semiring::TropicalWeight;
// Create transducer with version context
let mut transducer: ApiMigrationTransducer<TropicalWeight> =
ApiMigrationTransducer::new(
Version::new(1, 0), // Source version
Version::new(2, 0), // Target version
);
// Add migration rules
transducer.add_rule(ApiMigrationRule::rename_function(
"oldFn",
"newFn",
Version::new(1, 0),
Version::new(2, 0),
));
// Apply migration to tokens
let tokens = vec![
"call".to_string(),
"oldFn".to_string(),
"(".to_string(),
")".to_string(),
];
let result = transducer.migrate(&tokens);
// Check results
assert_eq!(result.migrated, vec!["call", "newFn", "(", ")"]);
assert_eq!(result.stats.rules_applied, 1);
assert_eq!(result.stats.automatic_migrations, 1);Fluent API for building transducers:
use lling_llang::programming::{ApiMigrationBuilder, ApiMigrationRule, Version};
use lling_llang::semiring::TropicalWeight;
let transducer: ApiMigrationTransducer<TropicalWeight> =
ApiMigrationBuilder::new(Version::new(1, 0), Version::new(2, 0))
.add_rule(ApiMigrationRule::rename_function(
"getUserById",
"fetchUser",
Version::new(1, 0),
Version::new(2, 0),
))
.add_rule(ApiMigrationRule::rename_type(
"UserData",
"User",
Version::new(1, 0),
Version::new(2, 0),
))
.add_rule(ApiMigrationRule::replace(
["callback", "(", "err", ",", "data", ")"],
["async", "/", "await"],
Version::new(1, 0),
Version::new(2, 0),
))
.build();Detailed feedback from migration operations:
use lling_llang::programming::{MigrationResult, MigrationStats};
// MigrationResult contains:
let result: MigrationResult = transducer.migrate(&tokens);
// Original input tokens
let original: &Vec<String> = &result.original;
// Transformed output tokens
let migrated: &Vec<String> = &result.migrated;
// IDs of rules that were applied
let applied: &Vec<String> = &result.applied_rules;
// Statistics
let stats: &MigrationStats = &result.stats;
println!("Rules applied: {}", stats.rules_applied);
println!("Automatic: {}", stats.automatic_migrations);
println!("Need review: {}", stats.manual_review_items);
println!("Total cost: {}", stats.total_cost);The simplest migration: renaming a function.
use lling_llang::programming::{ApiMigrationBuilder, ApiMigrationRule, Version};
use lling_llang::semiring::TropicalWeight;
let transducer: ApiMigrationTransducer<TropicalWeight> =
ApiMigrationBuilder::new(Version::new(1, 0), Version::new(2, 0))
.add_rule(ApiMigrationRule::rename_function(
"getUsers",
"fetchUsers",
Version::new(1, 0),
Version::new(2, 0),
))
.build();
// Before: getUsers()
let tokens = vec!["getUsers".to_string(), "(".to_string(), ")".to_string()];
// After: fetchUsers()
let result = transducer.migrate(&tokens);
assert_eq!(result.migrated, vec!["fetchUsers", "(", ")"]);Replace complex patterns involving multiple tokens.
use lling_llang::programming::{ApiMigrationBuilder, ApiMigrationRule, Version};
use lling_llang::semiring::TropicalWeight;
let transducer: ApiMigrationTransducer<TropicalWeight> =
ApiMigrationBuilder::new(Version::new(1, 0), Version::new(2, 0))
.add_rule(ApiMigrationRule::replace(
// Old: this.setState({...})
["this", ".", "setState"],
// New: setState(...)
["setState"],
Version::new(16, 8),
Version::new(18, 0),
))
.build();
// Before: this.setState({ count: 1 })
let tokens = vec![
"this".to_string(), ".".to_string(), "setState".to_string(),
"(".to_string(), "{".to_string(), "count".to_string(),
":".to_string(), "1".to_string(), "}".to_string(), ")".to_string(),
];
// After: setState({ count: 1 })
let result = transducer.migrate(&tokens);
assert_eq!(result.migrated[0], "setState");
assert_eq!(result.migrated[1], "(");Rules only apply within their version range.
use lling_llang::programming::{ApiMigrationBuilder, ApiMigrationRule, Version};
use lling_llang::semiring::TropicalWeight;
// Transducer for migrating from v1.0 to v2.0
let transducer: ApiMigrationTransducer<TropicalWeight> =
ApiMigrationBuilder::new(Version::new(1, 0), Version::new(2, 0))
// Rule for v1.0 → v1.5 only
.add_rule(ApiMigrationRule::rename_function(
"alpha",
"beta",
Version::new(1, 0),
Version::with_patch(1, 5, 0),
))
// Rule for v1.5 → v2.0 only
.add_rule(ApiMigrationRule::rename_function(
"beta",
"gamma",
Version::with_patch(1, 5, 0),
Version::new(2, 0),
))
.build();
// With source v1.0, only first rule applies
let tokens = vec!["alpha".to_string()];
let result = transducer.migrate(&tokens);
assert_eq!(result.migrated, vec!["beta"]);Flag deprecated APIs for manual review.
use lling_llang::programming::{ApiMigrationBuilder, ApiMigrationRule, Version};
use lling_llang::semiring::TropicalWeight;
let transducer: ApiMigrationTransducer<TropicalWeight> =
ApiMigrationBuilder::new(Version::new(1, 0), Version::new(2, 0))
.add_rule(ApiMigrationRule::deprecate(
"unsafeEval",
"REMOVED: Use safe alternatives instead",
Version::new(1, 0),
Version::new(2, 0),
))
.build();
let tokens = vec!["unsafeEval".to_string(), "(".to_string(), ")".to_string()];
let result = transducer.migrate(&tokens);
// Deprecation requires manual review
assert_eq!(result.stats.manual_review_items, 1);
assert_eq!(result.stats.automatic_migrations, 0);
// Output contains the warning message
assert!(result.migrated.contains(&"REMOVED: Use safe alternatives instead".to_string()));Convert migration rules to a WFST for composition with other transducers.
use lling_llang::programming::{ApiMigrationBuilder, ApiMigrationRule, Version};
use lling_llang::semiring::TropicalWeight;
use lling_llang::wfst::Wfst;
let transducer: ApiMigrationTransducer<TropicalWeight> =
ApiMigrationBuilder::new(Version::new(1, 0), Version::new(2, 0))
.add_rule(ApiMigrationRule::rename_function(
"old",
"new",
Version::new(1, 0),
Version::new(2, 0),
))
.build();
// Build WFST with cost mapping
let fst = transducer.build_wfst(TropicalWeight::new);
// WFST can be composed with other transducers
// (lexical normalizers, syntax checkers, etc.)Pre-built migration rules for common frameworks.
use lling_llang::programming::patterns;
// Get migration rules for React modernization
let react_rules = patterns::react_class_to_function();
// Includes transformations like:
// - componentDidMount → useEffect(() => { ... })
// - componentWillUnmount → useEffect(() => { return () => { ... }})
// - this.setState → setState
// - this.state → stateuse lling_llang::programming::patterns;
let python_rules = patterns::python2_to_python3();
// Includes transformations like:
// - print "x" → print("x")
// - xrange → range
// - raw_input → input
// - unicode → str
// - .iteritems() → .items()
// - .iterkeys() → .keys()
// - .itervalues() → .values()use lling_llang::programming::patterns;
let jquery_rules = patterns::jquery_to_vanilla_js();
// Includes transformations like:
// - $("#id") → document.getElementById("id")
// - $(".class") → document.querySelectorAll(".class")
// - .addClass() → .classList.add()
// - .removeClass() → .classList.remove()
// - .toggleClass() → .classList.toggle()
// - .attr() → .getAttribute()use lling_llang::programming::{ApiMigrationBuilder, Version, patterns};
use lling_llang::semiring::TropicalWeight;
// Combine pattern libraries with custom rules
let transducer: ApiMigrationTransducer<TropicalWeight> =
ApiMigrationBuilder::new(Version::new(2, 7), Version::new(3, 10))
// Add Python 2→3 rules
.add_rules(patterns::python2_to_python3())
// Add your custom rules
.add_rule(ApiMigrationRule::rename_function(
"my_legacy_fn",
"my_modern_fn",
Version::new(2, 7),
Version::new(3, 10),
))
.build();A typical migration workflow:
1. Define Version Context
├── Source version (current codebase)
└── Target version (desired API version)
↓
2. Collect Migration Rules
├── Use pattern libraries for known frameworks
├── Add custom rules for project-specific APIs
└── Mark breaking changes as manual_review
↓
3. Tokenize Source Code
└── Split code into token stream
↓
4. Apply Migration
├── Pattern matching against rules
├── Apply transformations
└── Track statistics
↓
5. Review Results
├── Automatic migrations: apply directly
└── Manual review items: human verification needed
↓
6. Reconstruct Code
└── Join tokens back into source code
Rules are indexed by their first token for
// Internal structure:
// rules_by_token: HashMap<String, Vec<ApiMigrationRule>>
//
// Lookup: O(1) for first token, then linear scan of matching rulesWhen multiple rules match, costs influence selection:
| Rule Type | Default Cost | Notes |
|---|---|---|
| Rename (function/type/param) | 0.1 | Low cost, highly automatic |
| Replace pattern | 0.2 | Slightly higher due to complexity |
| Deprecation | 1.0 | High cost, needs manual review |
Building a WFST is
- Composed with other transducers
- Determinized for faster matching
- Minimized to reduce state count
- WFST Composition: Composing transducers
- WFST Operations: Core WFST operations
- Determinization: Making transducers deterministic
- Semirings: Weight algebras for transducers