When a program does the wrong thing, the usual reaction is to add printing to it, run it, read the output, and take the printing out again. loft gives you a better tool: stop the program on the line you care about and look around.
The program used below is count.loft. It adds 1, 2 and 3 into a total.
Stop on a line
Say debug, then your file, then a colon and a line number:
$ printf ':continue\n' | loft debug count.loft:4
⏸ paused in main | total = 0, i = 1 (+2 compiler temp(s) — `:vars all`)
The program runs until it reaches line 4 and then waits for you. The line it prints tells you where you are and what your locals hold right now. The note at the end counts the variables the compiler made for itself — the loop's hidden index and a scratch slot — and :vars all shows those too:
$ printf ':vars all\n:quit\n' | loft debug count.loft:4
⏸ paused in main | total = 0, i#index = 1, i = 1, __work_1 = ""
Line 4 is inside a loop that goes round three times, so the program stops there three times. :continue means "carry on until you reach this line again", not "run to the end" — which is why the examples below say :continue more than once when they want the program to finish.
Look at a value
Type the name of a variable and press enter. Any expression works, not just a name — it is worked out where the program is paused, so it can combine the locals you are looking at:
$ printf 'total + i * 111\n:quit\n' | loft debug count.loft:4
111
This is the part that replaces printing. You do not have to guess in advance which values you will want; ask for them when you are there.
Move one step at a time
Four commands move the program forward:
:stepruns the next line, going INTO any function it calls:nextruns the next line, going OVER any function it calls:finishruns until the current function returns:continuecarries on to the next time this line is reached
The first three only differ when there IS a call, so the program here is steps.loft, whose loop calls add_up(i * 2) on line 9. Stopping on line 9 and stepping goes into it, and the paused line changes function:
$ printf ':step\n:quit\n' | loft debug steps.loft:9
⏸ paused in add_up | n = 2
:next at the same place runs the whole call and stays where you are:
$ printf ':next\n:quit\n' | loft debug steps.loft:9
⏸ paused in main | total = 2, i = 1
The program is now on the for line, about to start the next turn: total already holds this turn's result, and i still holds this turn's value.
And from inside add_up, :finish runs it out and lands back in the caller:
$ printf ':finish\n:quit\n' | loft debug steps.loft:2
⏸ paused in main | total = 0, i = 1
Watch a value instead of watching for it
Re-reading the paused line every time round the loop works, and :watch does it for you: carry on, and the program stops when that value changes, saying what it changed from and to:
$ printf ':watch total\n:continue\n:quit\n' | loft debug count.loft:4
watching total — :continue and the run stops when it changes
⏯ watchpoint: total changed 0 → 1
Change a value while it is running
You can write to a local, not only read it. This answers "would it work if this were right?" without editing the file and starting again:
$ printf 'total = 100\n:continue\n:continue\n:continue\n' | loft debug count.loft:4
total=106
run finished
The program carries on with the value you gave it. It finished with 106 instead of 6, because the loop still had 2 and 3 to add after the change.
Changing the loop variable moves the loop itself. Set i to 3 on the first turn and that turn adds 3 and is the last one, because the loop counts on from the value you gave it:
$ printf 'i = 3\n:continue\n' | loft debug count.loft:4
total=3
:undo takes an edit back, and :redo puts it on again:
$ printf 'total = 100\n:undo\ntotal + 55\n:quit\n' | loft debug count.loft:4
55
Getting out
:continue carries on. :quit stops right away. :help lists every command if you forget one — including the short forms :s, :n, :o and :c.
Typing, or feeding it a script
Every example here pipes its commands in, which is how they are checked automatically — the output you see above is compared against what the command really prints. Run loft debug count.loft:4 on its own and you get the (dbg) prompt to type at instead. The commands are the same either way.
fn main() {
This is what count.loft does — the program the examples above debug.
total = 0;
for i in 1..4 {
total += i;
}
assert(total == 6, "1 + 2 + 3 is 6, which is what the debugger shows you");
println("total={total}");
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Debugging
// @TITLE: Stop your program on a line and look at what it is really doing.
// When a program does the wrong thing, the usual reaction is to add printing to
// it, run it, read the output, and take the printing out again. loft gives you a
// better tool: stop the program on the line you care about and look around.
//
// The program used below is 'count.loft'. It adds 1, 2 and 3 into a total.
// ## Stop on a line
// Say 'debug', then your file, then a colon and a line number:
//
// $ printf ':continue\n' | loft debug count.loft:4
// ⏸ paused in main | total = 0, i = 1 (+2 compiler temp(s) — `:vars all`)
//
// The program runs until it reaches line 4 and then waits for you. The line it
// prints tells you where you are and what your locals hold right now. The note
// at the end counts the variables the compiler made for itself — the loop's
// hidden index and a scratch slot — and ':vars all' shows those too:
//
// $ printf ':vars all\n:quit\n' | loft debug count.loft:4
// ⏸ paused in main | total = 0, i#index = 1, i = 1, __work_1 = ""
//
// Line 4 is inside a loop that goes round three times, so the program stops
// there three times. ':continue' means "carry on until you reach this line
// again", not "run to the end" — which is why the examples below say
// ':continue' more than once when they want the program to finish.
// ## Look at a value
// Type the name of a variable and press enter. Any expression works, not just a
// name — it is worked out where the program is paused, so it can combine the
// locals you are looking at:
//
// $ printf 'total + i * 111\n:quit\n' | loft debug count.loft:4
// 111
//
// This is the part that replaces printing. You do not have to guess in advance
// which values you will want; ask for them when you are there.
// ## Move one step at a time
// Four commands move the program forward:
//
// - ':step' runs the next line, going INTO any function it calls
// - ':next' runs the next line, going OVER any function it calls
// - ':finish' runs until the current function returns
// - ':continue' carries on to the next time this line is reached
//
// The first three only differ when there IS a call, so the program here is
// 'steps.loft', whose loop calls 'add_up(i * 2)' on line 9. Stopping on line 9
// and stepping goes into it, and the paused line changes function:
//
// $ printf ':step\n:quit\n' | loft debug steps.loft:9
// ⏸ paused in add_up | n = 2
//
// ':next' at the same place runs the whole call and stays where you are:
//
// $ printf ':next\n:quit\n' | loft debug steps.loft:9
// ⏸ paused in main | total = 2, i = 1
//
// The program is now on the 'for' line, about to start the next turn: 'total'
// already holds this turn's result, and 'i' still holds this turn's value.
//
// And from inside 'add_up', ':finish' runs it out and lands back in the caller:
//
// $ printf ':finish\n:quit\n' | loft debug steps.loft:2
// ⏸ paused in main | total = 0, i = 1
//
// ## Watch a value instead of watching for it
// Re-reading the paused line every time round the loop works, and ':watch' does
// it for you: carry on, and the program stops when that value changes, saying
// what it changed from and to:
//
// $ printf ':watch total\n:continue\n:quit\n' | loft debug count.loft:4
// watching total — :continue and the run stops when it changes
// ⏯ watchpoint: total changed 0 → 1
//
// ## Change a value while it is running
// You can write to a local, not only read it. This answers "would it work if
// this were right?" without editing the file and starting again:
//
// $ printf 'total = 100\n:continue\n:continue\n:continue\n' | loft debug count.loft:4
// total=106
// run finished
//
// The program carries on with the value you gave it. It finished with 106
// instead of 6, because the loop still had 2 and 3 to add after the change.
//
// Changing the loop variable moves the loop itself. Set 'i' to 3 on the first
// turn and that turn adds 3 and is the last one, because the loop counts on from
// the value you gave it:
//
// $ printf 'i = 3\n:continue\n' | loft debug count.loft:4
// total=3
//
// ':undo' takes an edit back, and ':redo' puts it on again:
//
// $ printf 'total = 100\n:undo\ntotal + 55\n:quit\n' | loft debug count.loft:4
// 55
//
// ## Getting out
// ':continue' carries on. ':quit' stops right away. ':help' lists every command
// if you forget one — including the short forms ':s', ':n', ':o' and ':c'.
// ## Typing, or feeding it a script
// Every example here pipes its commands in, which is how they are checked
// automatically — the output you see above is compared against what the command
// really prints. Run 'loft debug count.loft:4' on its own and you get the
// '(dbg)' prompt to type at instead. The commands are the same either way.
fn main() {
// This is what `count.loft` does — the program the examples above debug.
total = 0;
for i in 1..4 {
total += i;
}
assert(total == 6, "1 + 2 + 3 is 6, which is what the debugger shows you");
println("total={total}");
}