Refactoring with Virtual Modules
The problem
You have a 500-line file. It works. You want to split it into modules. The traditional workflow:
- Create a new file
- Move functions there
- Add
pubto every moved declaration - Add
importin the original file - Prefix every call site with
module.(or add selective imports) - Hope you didn’t miss a dependency
If you got step 6 wrong, you find out after you’ve already made 200 edits. Tape solves this with compiler-validated refactoring.
Developer flow
Step 1: Wrap code you want to extract
// app.tape — your working file
import io from "io";
import math from "math";
var items: [64]i64;
var count: i64 = 0;
refactor("./store.tape") {
var items: [64]i64;
var count: i64 = 0;
// User chosen whitespace
fn add_item(val: i64) {items[count] = val;count = count + 1;}
// Sum comment
fn sum() -> i64 {
var total: i64 = 0;
for (i in 0..count) {
// Also comment here
total = total + items[i];
}
return total;
}
}
fn main() {
add_item(42);
add_item(7);
io.println("total: {sum()}");
}Step 2: Build normally
tape build app.tape -o app.exeThe compiler validates the refactor boundary:
add_itemandsumare called from host — they’ll need to bepub- Neither function references anything from the host that isn’t an import
- The block doesn’t use
ioormath— no imports needed in the target module
If you accidentally left a reference to a host-local variable inside the block, you’d see an error now — not after 200 manual edits.
Step 3: Iterate
Move more code in or out of the block. Every build validates the boundary. Keep going until you’re happy with the split.
// Oops, let's also extract the formatting:
refactor("./store.tape") {
var items: [64]i64;
var count: i64 = 0;
fn add_item(val: i64) { ... }
fn sum() -> i64 { ... }
fn format_items() -> string { ... } // new
}Step 4: Materialize
tape refactor app.tapeThis produces two files:
store.tape (generated):
pub {
var items: [64]i64;
var count: i64 = 0;
// User chosen whitespace
fn add_item(val: i64) {items[count] = val;count = count + 1;}
// Sum comment
fn sum() -> i64 {
var total: i64 = 0;
for (i in 0..count) {
// Also comment here
total = total + items[i];
}
return total;
}
fn format_items() -> string { ... } // new
}app.tape (rewritten):
import io from "io";
import math from "math";
import { items, count, add_item, sum } from "./store.tape";
fn main() {
add_item(42);
add_item(7);
io.println("total: {sum()}");
}Call sites are unchanged. The selective import brings the symbols into scope without a prefix.
CLI reference
tape refactor <file> # materialize all refactor blocks
tape refactor <file> --dry-run # preview output without writing files
tape refactor <file> --force # overwrite if target files exist—dry-run
Shows what would be generated:
Virtual module "./store.tape":
Imports: (none)
Exports: items, count, add_item, sum
Host "app.tape" after extraction:
New import: import { items, count, add_item, sum } from "./store.tape";
Removed: refactor block (lines 12-31)
Would write: ./store.tape (new file)
Would rewrite: app.tape—force
By default, tape refactor errors if the target file already exists. Use --force to overwrite.
Multiple targets
Extract to several modules at once:
import io from "io";
import math from "math";
fn main() {
let v = make_vec(1.0, 2.0, 3.0);
io.println(format_vec(v));
}
refactor("./veclib.tape") {
struct Vec3 { x: f64; y: f64; z: f64; }
fn make_vec(x: f64, y: f64, z: f64) -> Vec3 {
return Vec3 { x: x; y: y; z: z; };
}
fn vec_len(v: Vec3) -> f64 {
return math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
}
}
refactor("./display.tape") {
fn format_vec(v: Vec3) -> string {
return "{v.x}, {v.y}, {v.z}";
}
}The compiler resolves that display needs Vec3 from veclib. After materialization:
veclib.tapegetsimport math from "math"(used byvec_len)display.tapegetsimport { Vec3 } from "./veclib.tape"(used byformat_vec)app.tapegets selective imports from both
Why this works
The refactor block is not a preprocessor trick or a post-hoc tool. It’s a compiler-enforced module boundary that happens to live inside a single file. This means:
- Errors show up during development, not when you run a refactoring tool
- You can iterate — move code in and out of blocks, build each time
- The tool is trivial — by the time
tape refactorruns, everything is already validated - No broken intermediate states — the program works the entire time
Constraints
- Refactor blocks can only appear in
#coderegion - Code inside cannot reference host-local symbols (only imports and other refactor block contents)
- Circular dependencies between blocks are forbidden
- The host file’s
@profileis inherited by generated modules
Last modified: