Tuesday, August 11, 2026

The Secret Language of Programmers: A Glossary of Code Slang

 

Programmer Slang: A Categorized Glossary

Implementation Style

Term

Meaning

hand-rolled

Built from scratch instead of using an existing library/framework

roll your own (RYO)

Same idea — DIY instead of reusing existing code

vanilla

Plain, unmodified (e.g. "vanilla JS")

bare metal

Running directly on hardware, no OS/abstraction layer

from scratch

Built without reusing existing code

duct tape / bandaid fix

A quick, ugly, temporary fix

hack

An inelegant but functional solution

State & Tracking

Term

Meaning

dirty tracking

Marking data as modified so a system knows what needs re-saving or re-syncing, instead of redoing everything

stale data

Cached data that's out of date

race condition

A bug caused by timing-dependent execution order

poison pill

A special message/value used to signal "stop" to a consumer

heisenbug

A bug that disappears or changes behavior when you try to observe it

bit rot

Code that degrades in usefulness simply because the world around it changed

zombie process

A finished process still lingering in the process table

Debugging & Investigation

Term

Meaning

smoking gun

Definitive evidence pinpointing the exact cause of a bug

root cause

The actual underlying source of a bug, as opposed to a symptom

red herring

A misleading clue while debugging

breadcrumbs

Logging or trail left behind to trace what happened

rubber duck debugging

Explaining code line-by-line to something (or someone) to find the bug yourself

war room

Emergency incident response meeting

canary

Something deployed early or small to catch problems before wider rollout

Code Quality & Architecture

Term

Meaning

spaghetti code

Tangled, hard-to-follow logic

boilerplate

Repetitive code you have to write every time

magic number/string

An unexplained literal value buried in code

god object/class

One class that does far too much

shim

A small compatibility layer bridging two incompatible interfaces

glue code

Code that just connects other pieces together, with no real logic of its own

lava flow

Old, confusing code nobody understands or dares remove

footgun

A feature that makes it easy to shoot yourself in the foot

Team & Process

Term

Meaning

yak shaving

Doing a seemingly pointless task that's secretly a prerequisite for what you actually wanted to do

bikeshedding

Arguing over trivial details while ignoring the real issue

cargo culting

Copying practices or code without understanding why they work

tribal knowledge

Undocumented knowledge that only exists in people's heads

bus factor

How many people would need to disappear before a project is in serious trouble

works on my machine

The classic excuse when a bug can't be reproduced elsewhere

dogfooding

Using your own product internally before shipping it

Git-Specific

Term

Meaning

cherry-pick

Apply a specific commit from one branch onto another

bisect

Binary-search through commit history to find which commit introduced a bug

rebase hell

Painful, conflict-heavy history rewriting

squash

Combine multiple commits into one

force push

Overwrite remote history — dangerous, can erase others' work

detached HEAD

Checked out a commit instead of a branch; easy to lose work

Concurrency

Term

Meaning

deadlock

Two or more processes stuck waiting on each other forever

livelock

Processes keep changing state in response to each other but make no real progress

starvation

A process never gets the resources it needs because others keep hogging them

thundering herd

Many processes/threads all waking up and hitting a resource at once

priority inversion

A low-priority task ends up blocking a high-priority one

Performance

Term

Meaning

memory leak

Memory that's allocated but never freed

thrashing

A system spends more time managing resources than doing real work

hot path

The most frequently executed code path — worth optimizing

cold start

The slow first run of something, e.g. a serverless function warming up

N+1 problem

A single query accidentally becomes N+1 queries in a loop

Networking

Term

Meaning

handshake

Initial exchange to establish a connection

keep-alive

Mechanism to prevent a connection from closing due to inactivity

man-in-the-middle

An attacker secretly intercepting communication between two parties

jitter

Variation in latency over time

split brain

A distributed system's nodes disagree on state after a network partition

Testing & QA

Term

Meaning

flaky test

A test that passes or fails inconsistently with no code changes

happy path

The main, no-errors-expected flow through code

edge case

An input or scenario at the extreme boundary of what's expected

smoke test

A quick, shallow test to confirm nothing's catastrophically broken

regression

A bug that reintroduces a previously fixed issue

Compiler & Low-Level

Term

Meaning

undefined behavior (UB)

Code whose behavior isn't defined by the language spec — anything can happen

dead code elimination

Compiler optimization that strips out unreachable code

padding

Extra bytes inserted, e.g. in structs, for memory alignment

register spilling

When there aren't enough CPU registers, values get pushed to memory

canary value

A known value placed in memory to detect stack buffer overflows

 

Monday, August 10, 2026

Skipping a Stuck delay() with Interrupts on Arduino

Skipping a Stuck delay() with Interrupts on Arduino

If you've ever written Arduino firmware with a long delay() or a busy-wait loop, you've probably run into this problem: the microcontroller is stuck waiting, and there's no clean way to say "never mind, move on" without rebooting or redesigning your whole control flow.

The instinct a lot of people reach for — myself included, the first time I hit this — is to interrupt the wait and literally redirect execution somewhere else. It turns out there are two very different ways to attempt that, one fragile and one solid. Here's the story of both.

The tempting (but broken) approach: rewriting the return address

The idea sounds elegant on paper. When an interrupt fires on AVR, the current return address gets pushed onto the stack. So why not reach into the stack from inside the ISR, overwrite that address, and force execution to resume somewhere else entirely when the ISR exits?

Here's roughly what that looks like:

volatile bool skipFunction = false;

void nextFunction() {
    Serial.println("Jumped to next function!");
}

void customDelay(unsigned long duration) {
    unsigned long start = millis();
    while (millis() - start < duration) {
        if (skipFunction) break;
    }
    Serial.println("Exited custom delay!");
}

ISR(INT0_vect) {
    void (*nextFuncPtr)() = nextFunction;

    asm volatile (
        "ldi r30, lo8(%0) \n"
        "ldi r31, hi8(%0) \n"
        "sts 0x5D, r30 \n"
        "sts 0x5E, r31 \n"
        :
        : "r" (nextFuncPtr)
        : "r30", "r31"
    );

    skipFunction = true;
}

It reads convincingly: attach an interrupt to a pin, and when the pin fires, hand-craft your way into a different function via inline assembly.

The problem is that 0x5D and 0x5E aren't the return address at all — they're SPL and SPH, the AVR stack pointer registers. Writing nextFuncPtr into them doesn't touch the saved return address; it just relocates the stack pointer itself somewhere else in SRAM. The actual return address lives in the two bytes the stack pointer is pointing at (with a one-byte offset, and stored big-endian, word-addressed), not in the pointer registers themselves. On real hardware, this code would corrupt your stack rather than redirect execution.

There's a second issue too: attachInterrupt(digitalPinToInterrupt(2), ISR, RISING) passes ISR as if it were an ordinary callback, but ISR(INT0_vect){...} is the raw AVR vector macro — the two mechanisms don't mix. You'd need to pick one: a normal attachInterrupt callback, or a hand-registered vector with PCICR/PCMSK, not both at once.

None of this is meant as a knock — return-address manipulation is a real, well-documented technique in lower-level embedded and systems work. It's just genuinely easy to get subtly wrong, architecture-specific, and risky to debug when it misbehaves, which is exactly why it's worth pausing on before reaching for it.

The approach that actually works: a flag

Here's the thing — you don't need to touch the stack at all to skip a running delay. delay()-style code is just a loop checking elapsed time. An ISR doesn't need to "jump into" that loop; it just needs to set a flag the loop is already checking.

volatile bool skipDelay = false;

void skippableDelay(unsigned long duration) {
    unsigned long start = millis();
    while (millis() - start < duration) {
        if (skipDelay) break;   // ISR set this — bail out early
    }
    skipDelay = false;          // reset for next time
}

void onButtonPress() {
    skipDelay = true;           // ISR: just flip the flag, nothing else
}

void setup() {
    Serial.begin(9600);
    pinMode(2, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(2), onButtonPress, FALLING);
}

void loop() {
    Serial.println("Starting delay...");
    skippableDelay(5000);
    Serial.println("Delay finished (or skipped)!");
}

This is simpler, safer, and — importantly — portable. It works identically on AVR, ESP32, and ARM Cortex boards, because it doesn't depend on any chip-specific stack layout. Flipping a volatile bool inside an ISR is about as close to "instant and side-effect free" as embedded code gets, which is exactly what you want inside an interrupt handler in the first place.

If you need to skip through multiple stacked delays or steps rather than just one, the same pattern extends naturally into a counter:

volatile uint8_t skipCount = 0;

void onButtonPress() {
    skipCount++;
}

Each skippable block decrements skipCount and breaks early if it's still nonzero, letting repeated button presses skip through a queue of steps one at a time.

The one real limitation

This trick works because you're polling inside your own loop. It does not work if you call the built-in Arduino delay() function directly — that one blocks at a lower level and never checks any flag, interrupt-driven or otherwise. The moment you want skippable waits, you have to write your own polling loop (as above) instead of calling delay().

But what if you actually want to jump to an address?

The flag approach solves "skip a delay" completely. But it's worth asking the more general question it sidesteps: is there a way to actually jump to a known function address from an ISR — properly this time, not the broken SPL/SPH version — and have it work?

The answer is yes, but it takes more than just fixing which registers you write to.

Why patching the right stack bytes still isn't enough. If you correctly locate the return address on the stack (rather than the SPL/SPH registers) and patch it in place, you fix the immediate crash — but the current function's stack frame is still sitting there, abandoned, never properly unwound. The moment the function you jumped into tries to ret, it pops leftover garbage from that orphaned frame and jumps into nonsense.

The correct fix is closer to a manual longjmp: snapshot the stack pointer before entering the section you might skip, and in the ISR, reset SP wholesale back to that snapshot — discarding the entire abandoned frame at once — then jump directly, rather than patching two bytes in place.

volatile uint16_t savedSP; // stack pointer snapshot, taken before entering the skippable section

void nextFunction() __attribute__((used, noinline));

void nextFunction() {
    Serial.println("Jumped to next function!");
    while (1) { /* dead end on purpose - see below */ }
}

// Naked ISR: no compiler-generated prologue/epilogue, we control everything
ISR(INT0_vect, ISR_NAKED) {
    asm volatile (
        "lds  r16, savedSP      \n"
        "lds  r17, savedSP+1    \n"
        "cli                    \n"
        "out  __SP_L__, r16     \n"
        "out  __SP_H__, r17     \n"   // stack pointer reset to pre-call snapshot
        "sei                    \n"
        "ldi  r30, lo8(pm(nextFunction)) \n"
        "ldi  r31, hi8(pm(nextFunction)) \n"
        "ijmp                   \n"   // jump directly, no return expected
        ::: "r16", "r17", "r30", "r31"
    );
}

void loop() {
    savedSP = SP; // snapshot taken right before the skippable call
    customDelay(5000);
}

This version genuinely works on real AVR hardware — the stack pointer really does get reset to a known-good point rather than corrupted. But notice the cost: nextFunction() can never return. There's no valid return address left anywhere for it to pop; we discarded that whole frame on purpose. It has to be a dead end (as shown), or explicitly jump somewhere else itself. This is a one-way context switch, not a function call — the same underlying idea a bootloader or an RTOS task-switcher uses, not "call and come back."

Getting back: setjmp / longjmp

If you actually need to return to where you jumped from — run nextFunction(), then continue loop() normally afterward — the one-way stack reset above can't do that. What you want is the standard C mechanism built for exactly this: setjmp/longjmp, which avr-libc supports.

#include <setjmp.h>

jmp_buf loopContext; // saved CPU context: SP, PC, callee-saved registers
volatile bool jumpRequested = false;

void onButtonPress() {
    jumpRequested = true; // ISR stays trivial - flag only
}

void customDelay(unsigned long duration) {
    unsigned long start = millis();
    while (millis() - start < duration) {
        if (jumpRequested) {
            jumpRequested = false;
            nextFunction(); // called normally - nextFunction ends with longjmp back
        }
        delay(200);
    }
}

void nextFunction() {
    Serial.println("Running...");
    delay(1000);
    longjmp(loopContext, 1); // unwinds straight back to the setjmp() in loop()
}

void loop() {
    if (setjmp(loopContext) == 0) {
        customDelay(5000); // normal path
    } else {
        Serial.println("Back in loop via longjmp!"); // landed here after longjmp
    }
}

setjmp(loopContext) records the stack pointer and registers at that exact point in loop(). Calling longjmp(loopContext, 1) from anywhere later — deep inside customDelay(), from nextFunction() — unwinds the stack back to that saved point and makes setjmp() return again, this time with the value 1. It's a real two-way jump, and it has real advantages over the hand-rolled asm version: it saves the full register set the AVR calling convention actually requires (not just SP), so it's specified behavior rather than "probably works"; there's no naked ISR or inline asm; and <setjmp.h> exists on ESP32 and ARM too, so unlike the raw-asm version, it isn't AVR-only.

Two caveats matter here, and they're easy to get wrong:

  • Never call longjmp directly from an ISR. It isn't signal-safe — you'd skip the reti that re-enables the AVR's global interrupt flag, leaving interrupts silently dead with no crash to tip you off. The pattern above avoids this correctly: the ISR only sets jumpRequested; the actual longjmp happens later, from nextFunction(), called as ordinary code from inside customDelay()'s loop, well outside ISR context.
  • longjmp doesn't run C++ destructors for objects that go out of scope during the unwind, and it doesn't restore hardware/peripheral state either — if customDelay() had half-toggled a GPIO sequence or left an SPI transaction open, that's abandoned exactly like the local variables are. For plain-data sketches like this one, there's nothing to bypass; the moment you add a C++ object with real cleanup logic on this call path (a File, a buffer you own), that's a sign longjmp is the wrong tool for that spot.

Hardening it: three rounds of fixes

The version above works — but building on it a bit further surfaces real problems, each one worth knowing about even if you never hit them in your own sketch.

Round 1 — re-entrancy. What happens if the button is pressed a second time while nextFunction() is already mid-flight, during its own 1-second "work" delay? customDelay()'s loop would see jumpRequested set again and call nextFunction() a second time, nested inside the first call. The fix is a second flag guarding against exactly this:

volatile bool jumpInProgress = false;

void nextFunction() {
    jumpInProgress = true;
    Serial.println("Running...");
    delay(1000);
    longjmp(loopContext, 1);
}

and in customDelay(), only entering nextFunction() if a jump isn't already in progress. It's also worth reading and clearing jumpRequested under noInterrupts()/interrupts(), so the check-and-clear happens as one atomic step rather than two — on a single bool this is effectively atomic on AVR anyway, but wrapping it makes the intent explicit and keeps things correct if the flag type ever changes.

Round 2 — wrapping it up cleanly. Once there are two coordination flags plus the jump target itself, it's worth grouping them into one small class rather than three loose globals:

class JumpContext {
public:
    jmp_buf context;
    void requestJump() { requested_ = true; }
    bool consumeRequest() {
        noInterrupts();
        bool r = requested_;
        requested_ = false;
        interrupts();
        return r;
    }
    void begin() { inProgress_ = true; }
    void end()   { inProgress_ = false; }
    bool inProgress() const { return inProgress_; }
private:
    volatile bool requested_  = false;
    volatile bool inProgress_ = false;
};

One subtlety: setjmp() itself still has to be called directly inside loop(), not wrapped inside a method on this class. setjmp() only saves a valid return point for the stack frame of whichever function calls it directly — if a method wrapped the call, that method's frame would already be gone by the time it returned, making any later jump into it undefined behavior.

This is also a natural place to enforce the destructor caveat from earlier at compile time instead of just documenting it in a comment:

#define JUMP_SAFE_LOCAL(Type, name, initExpr)                          \
    static_assert(std::is_trivially_destructible<Type>::value,         \
        #Type " has a non-trivial destructor - unsafe on the "         \
        "setjmp/longjmp path");                                        \
    Type name = (initExpr)

Any local declared this way on the jump path fails to compile if it has real cleanup logic — catching the mistake before it ever runs, rather than relying on remembering the rule.

Round 3 — failing loud, and a cooperative timeout. Two more things are worth hardening once the pattern is otherwise solid. First, calling longjmp on a jmp_buf that setjmp was never actually run on is undefined behavior — it can jump to a garbage stack pointer and program counter. It's better to check and halt with a clear message than corrupt memory silently:

void jumpTo(int value) {
    if (!isSet_) {
        Serial.println("FATAL: jumpTo() before setjmp() - halting.");
        while (true) { delay(1000); }
    }
    longjmp(buf_, value);
}

Second, nextFunction() can be given a cooperative timeout, so if its work runs long it jumps back on its own rather than blocking indefinitely:

const unsigned long JUMP_TIMEOUT_MS = 3000;

void nextFunction() {
    unsigned long workStart = millis();
    while (millis() - workStart < WORK_DURATION_MS) {
        if (millis() - workStart > JUMP_TIMEOUT_MS) {
            jumpTo(2); // distinct code: timeout, not a normal finish
            return;
        }
        delay(50);
    }
    jumpTo(1);
}

This only works because nextFunction() polls the clock itself between chunks of work — it's a cooperative timeout, not a preemptive one. It still can't rescue you from a call that blocks without ever returning control, like a hung I2C read; firing longjmp from a timer ISR to force that would run straight into the signal-safety problem from earlier.

Takeaways

  • Debounce mechanical buttons feeding your interrupt, or you'll get spurious triggers.
  • Keep ISRs short. Setting a flag is ideal; heavy logic, register surgery, or longjmp inside an ISR invites hard-to-debug timing bugs or silently dead interrupts.
  • Wrap multi-step critical sections in noInterrupts() / interrupts() if a second interrupt firing mid-update could leave shared state inconsistent.
  • Stack/return-address hacking is architecture-specific and fragile — AVR, ESP32, and ARM all lay out their stacks differently, and it's very easy to target the wrong registers or leave a frame unbalanced.
  • setjmp/longjmp is the right tool if you actually need a two-way jump — but keep the jump path free of C++ objects with real destructors, guard against re-entrancy, and never call longjmp from inside an ISR.

For nearly every "I need to bail out of a wait" situation on Arduino, a flag — or a small state machine if you need to skip through several stages — gets you there with far less risk than reaching for the stack. And if you do need a genuine jump-and-return, setjmp/longjmp, built out carefully as above, is the properly-scoped way to get it.