SmartKitio

Programming

What Actually Happens When Code Runs?

The program on your screen is text. The program that runs is memory, registers, and syscalls changing over time. Between those worlds sits a pipeline—lexing, parsing, analysis, code generation, optimization, and runtime services—that explains syntax errors, stack traces, and the gap between “works on my machine” and production.

8 min read · Updated 2026-10-04

Developer workspace with code on screen and warm desk lighting
Photo via Unsplash · Unsplash License

Source code is not yet a program

Files of source code are characters encoded in UTF-8 (or another encoding your toolchain expects). The operating system does not execute text directly—it loads an executable format (ELF, Mach-O, PE) or starts a runtime that interprets bytecode. The first job of a language implementation is to turn text into a structured representation it can reason about.

From text to structure

Classic compilation / execution pipeline
  1. Source

    Human-written .rs, .java, .js, .py files

  2. Lexer

    Scan characters into tokens (keywords, identifiers, literals)

  3. Parser

    Build an abstract syntax tree (AST) from grammar rules

  4. Semantic analysis

    Types, scopes, undefined variables—errors before run

  5. IR / bytecode / machine code

    Lower AST to portable ops or native instructions

  6. Optimizer

    Inline, dead-code eliminate, reorder for CPU (optional pass)

  7. Runtime / CPU

    Execute on hardware or virtual machine with OS services

Exact stages vary by language; the direction of data flow is the same.

Syntax errors fail in the lexer or parser (“unexpected token”). Type or name errors often fail in semantic analysis. Logic errors pass all checks and only appear when specific inputs reach buggy branches—that is why tests and observability matter after the pipeline succeeds.

Interpreted vs compiled

Pure interpretation walks the AST or bytecode and performs actions immediately—simple to bootstrap, slower per step. Ahead-of-time (AOT) compilation translates entire programs to machine code before execution—C, Rust, and Go release builds behave this way. Many popular languages blend both: Python compiles to bytecode then interprets; Java ships bytecode and JIT-compiles hot methods; JavaScript parses once, runs fast paths through tiers of JIT, and falls back to slower interpretation when types change.

Execution strategies (simplified)

StrategyIdeaExamples
Tree-walking interpretVisit AST nodes and actEarly teaching interpreters
Bytecode + VMCompile to portable instructions; VM dispatches opcodesCPython bytecode, Java JVM, C# IL
AOT native compileMachine code before runRust, C release, Go default
JITCompile hot paths at runtimeJavaScript engines (V8, SpiderMonkey), JVM HotSpot
Transpile then runSource-to-source, then existing runtimeTypeScript → JavaScript

Interpreter vs AOT vs JIT (when each wins)

ApproachWhen work happensStartupPeak speedTypical tradeoff
Interpreter / bytecode VMEach instruction at run timeFastLower until warmed upPortable, predictable builds; hot loops cost more
AOT compileBefore the process startsDepends on binary sizeHigh for native codeLonger builds; must ship per CPU/OS; errors caught early
JITFirst runs interpret; hot code compiled laterFast enough to browseVery high on stable pathsProfiling overhead; deopt when assumptions break

Choosing among them is rarely ideological. Mobile games and CLI tools favor AOT binaries with no VM surprise. Servers running long-lived Node or JVM processes amortize JIT warmup. Short scripts and REPL sessions benefit from interpretation without waiting for optimization. Your job as a developer is to know which pipeline your runtime uses so you interpret profiles and stack traces correctly—not to memorize every engine brand.

Bytecode and virtual machines

Bytecode is a compact instruction set for a virtual machine—not tied to one CPU architecture. Java .class files, Python .pyc, and WebAssembly modules target VMs that enforce memory safety or sandboxing policies. The VM handles calling conventions, exceptions, and sometimes just-in-time compilation to native code for hot loops.

The VM sits between your source and the OS: it can verify bytecode before execution (Java), enforce sandbox rules (browser WASM), or insert hooks for debuggers and profilers. That indirection costs CPU but buys portability— the same .jar runs on ARM and x86—and uniform error models like Java exceptions instead of segfaults from bad pointers. When you attach a debugger to Node or Python, you are often stopping the VM between bytecode instructions, then mapping back to source lines through debug metadata generated at compile time.

JIT compilation in modern JavaScript

Browser and Node.js engines parse JavaScript, build internal structures, and execute quickly via interpreters. Profiling finds hot functions; JIT compilers (often multi-tier) emit optimized machine code specialized for observed types. Deoptimization happens when assumptions break— which is why monomorphic call sites are faster than constantly changing types in tight loops.

Multi-tier JITs start in a cheap baseline tier, then promote functions that run often into optimizing compilers that specialize for integers, doubles, or object shapes actually observed at call sites. When a property type flips from number to string, the engine may bail out to a slower path and re-profile. That is why micro-benchmarks lie: a function fast in isolation may deopt inside a larger app where types vary. WASM modules skip much of this dance by arriving pre-compiled with explicit types, at the cost of a different toolchain.

Memory: stack vs heap

Think of the stack as scratch paper for the current function call chain, and the heap as a bulletin board where objects live until nothing points at them. When main calls fetchUser, which calls parseJson, each call pushes a frame: locals like id and temp buffers sit in that frame and vanish when the function returns. When parseJson returns a new object, that object’s memory comes from the heap because the caller still needs it after parseJson’s frame is popped.

  • The call stack holds activation records: local variables, return addresses, and stack frames for each function call.
  • Stack overflow means too deep recursion or runaway mutual calls—not “too many variables” in the abstract.
  • The heap holds objects with dynamic lifetimes: arrays, closures, and anything outliving a single frame.
  • Languages differ: Rust tracks ownership at compile time; C expects manual free; Java and JS use garbage collectors.
javascript
function total(items) {
  let sum = 0;           // stack: primitive in this frame
  for (const x of items) {
    sum += x.price;      // stack: loop locals
  }
  return { sum, count: items.length }; // heap: new object outlives this frame
}
const cart = [{ price: 9 }, { price: 4 }];
const result = total(cart); // stack: result reference; heap: { sum, count }

Garbage collection

Tracing garbage collectors find objects reachable from roots (globals, stacks, registers) and reclaim the rest. Pauses depend on heap size, collector algorithm, and whether the runtime uses generational or concurrent collection. Memory leaks in GC languages still happen when you retain references to objects you no longer need—GC frees unreachable objects, not forgotten ones.

Operating system and CPU

Machine instructions load values into registers, branch, call into shared libraries, and issue syscalls for file I/O, network sockets, and memory mapping. The kernel schedules threads, enforces permissions, and handles page faults when physical RAM fills. Your language runtime sits above that— but profiling still shows syscall and lock contention when code “feels slow.”

Debugging across the pipeline

  • Syntax highlighting ends where the parser starts—errors are static.
  • Breakpoints and step-debuggers map to bytecode or machine code with debug info.
  • console.log and structured logging observe runtime state without stopping time.
  • Reproducing prod bugs often needs the same build flags, environment variables, and data shapes—not just the same source file.

Observability tools sit at the runtime boundary: profilers sample stacks (stack frames again), tracers follow syscalls, and heap snapshots enumerate live heap objects. When production only ships minified bundles, enable source maps in your error tracker for symbolic stacks; otherwise you are debugging generated names like n and t instead of fetchUser and parseJson. Local dev that skips minification hides that gap until the first deploy.

Production realities

Shipping code adds bundlers, minifiers, tree-shaking, and environment-specific configuration. Server-side runtimes add process managers, containers, and horizontal scaling—each instance runs the same pipeline independently. Client-side code adds download, parse, compile, and execute budgets on the main thread, which is why our browser and client-vs-server articles pair with this one.

Source maps and minification

Minifiers rename variables to single letters and drop whitespace so bundles download faster; the CPU still executes valid JavaScript, but line numbers in errors no longer match your editor. Source maps are sidecar files (or inline comments) that map bundled positions back to original TypeScript or JSX. DevTools and some log aggregators consume them when you deploy maps alongside assets—or upload them to error trackers. Without maps, a failure at bundle.js:1:48291 is real execution state you must decode; with maps, you see the same stack trace you would in local dev. Never ship source maps to public URLs if they embed full source unless you intend that exposure; many teams restrict maps to internal symbol servers.

What to remember

  • Source text passes through lex, parse, and analysis before any CPU work; static errors and runtime errors happen at different stages.
  • Interpreters, bytecode VMs, AOT, and JIT are points on a spectrum—most languages you use combine more than one.
  • Stack frames are per-call scratch space; heap objects survive across calls until the runtime reclaims or frees them.
  • Production transforms (minify, bundle, optimize) change filenames and line numbers unless source maps bridge the gap.
  • Profiling and logging observe the runtime that actually ran, not the file you wish had run.

Try SmartKitio’s json-formatter and regex-tester in the browser: they execute in the same JavaScript VM and JIT pipeline this article describes, so pasted payloads stay on your machine while you explore parsing, matching, and runtime behavior without deploying code.

Deeper dives

  1. Crafting Interpreters (book)
  2. MDN — JavaScript engine fundamentals
  3. Aho, Lam, Sethi, Ullman — Compilers: Principles, Techniques, and Tools

Related articles