BPVM Snack Pack #12 - Statements 101: The Language Before Bytecode
Before your nodes become bytecode, they become statements. Think of them as the intermediate language between visual nodes and machine code.
TL;DR — Blueprint compilation turns visual nodes into FBlueprintCompiledStatement objects, an intermediate language sitting between nodes and bytecode. Each statement carries a type (KCST_CallFunction, KCST_Assignment, KCST_GotoIfNot, etc.), an LHS output terminal, and RHS input terminals. This layer enables optimization, platform-independent output, and validation before bytecode generation.
The content in this post is based on Unreal Engine 5.6.0
BPVM Snack Pack - Quick Blueprint knowledge drops! Part of the Blueprint to Bytecode series.
The Translation Pipeline
Your Blueprint nodes go through three forms:
1
2
Visual Nodes → Statements → Bytecode
(What you see) → (Intermediate) → (What runs)
Statements are the middle ground - more structured than nodes, simpler than bytecode!
Meet FBlueprintCompiledStatement
Every operation becomes a statement:
1
2
3
4
5
6
7
8
struct FBlueprintCompiledStatement
{
EKismetCompiledStatementType Type; // What kind of operation?
FBPTerminal* LHS; // Left side (usually output)
TArray<FBPTerminal*> RHS; // Right side (inputs)
UFunction* TargetFunction; // For function calls
UEdGraphNode* SourceNode; // Where it came from
};
Think of it as a recipe card for one operation!
The Statement Types
There are 30+ statement types. Here are the essentials:
1
2
3
4
5
6
7
8
9
10
11
enum EKismetCompiledStatementType
{
KCST_Nop = 0, // Do nothing
KCST_CallFunction = 1, // Call a function
KCST_Assignment = 2, // Set a variable
KCST_CompileError = 3, // Compilation failed
KCST_UnconditionalGoto = 4, // Jump to label
KCST_Return = 7, // Return from function
KCST_SwitchValue = 29, // Select/switch statement
// ... many more
};
Each type tells the backend exactly what to generate!
Real Example: Print String
Your “Print String” node becomes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The node
UK2Node_CallFunction "PrintString"
// Becomes this statement
FBlueprintCompiledStatement {
Type: KCST_CallFunction
TargetFunction: "PrintString"
RHS: [Terminal_StringValue] // "Hello World"
}
// Eventually becomes bytecode
0x44 EX_CallFunc
0x08 PrintString
"Hello World"
0x53 EX_Return
Assignment Statements
Setting a variable:
1
2
3
4
5
6
7
// Blueprint: Health = 100
Statement {
Type: KCST_Assignment
LHS: Terminal_Health // Target variable
RHS: [Terminal_100] // Value to assign
}
LHS = “Left Hand Side” (where to put it) RHS = “Right Hand Side” (what to put there)
Control Flow Statements
Branches and jumps:
1
2
3
4
5
6
7
8
9
10
11
12
// Branch node
Statement {
Type: KCST_GotoIfNot
LHS: Terminal_Condition // What to check
TargetLabel: Label_False // Where to jump if false
}
// Unconditional jump
Statement {
Type: KCST_UnconditionalGoto
TargetLabel: Label_End
}
These become the skeleton of your logic flow!
The Terminal System
Statements use FBPTerminal for data:
1
2
3
4
FBPTerminal* Terminal = new FBPTerminal();
Terminal->Type = "int32";
Terminal->Name = "MyVariable";
Terminal->Source = OutputPin; // Where it connects
Terminals are placeholders for values - like variables in assembly!
Why Statements?
Why not go straight to bytecode?
- Optimization Opportunity ```cpp // Before optimization Statement1: A = B + 1 Statement2: C = A Statement3: D = C
// After optimization Statement1: D = B + 1 // Merged!
1
2
3
4
5
6
7
2. **Platform Independence**
```cpp
// Same statements can generate:
- Bytecode (for VM)
- C++ code (for nativization)
- Debug output (for tools)
- Easier Validation
1 2 3 4
// Check for errors at statement level if (Statement.LHS == nullptr) { Error("Assignment has no target!"); }
The Compilation Flow
1
2
3
4
5
6
7
8
9
10
11
12
void CompileNode(UK2Node* Node)
{
// Step 1: Node handler creates statements
FNodeHandlingFunctor* Handler = GetHandler(Node);
Handler->Compile(Context, Node);
// Step 2: Statements go into context
Context.AllGeneratedStatements.Add(NewStatement);
// Step 3: Backend converts to bytecode (later)
Backend.GenerateBytecode(Context.AllGeneratedStatements);
}
Statement Optimization
Before becoming bytecode, statements get optimized:
1
2
3
4
5
6
7
8
9
10
11
// Adjacent gotos
Goto Label1
Label1: // Removed!
// Dead code
Return
CallFunction // Never reached - removed!
// Redundant assignments
A = B
A = C // First one removed!
Debug Sites
Special statements for debugging:
1
2
3
4
Statement {
Type: KCST_DebugSite
SourceNode: MyNode // Breakpoint here!
}
These become breakpoint locations in the debugger!
Quick Takeaway
- Statements are the intermediate language between nodes and bytecode
- Each statement has a type (what to do) and terminals (data)
- LHS = output/target, RHS = inputs/sources
- Statements enable optimization before bytecode generation
- They’re platform-independent (can generate different outputs)
- Think of them as assembly language for Blueprint!
The Assembly Line
Your nodes go through this pipeline:
- Visual node (what you see)
- Statement (structured operation)
- Bytecode (what runs)
Statements are where the real compilation happens!
Want More Details?
For complete statement breakdown:
- From Blueprint to Bytecode I - FBlueprintCompiledStatement
- From Blueprint to Bytecode IV - Statement Processing
Next: How the DAG Scheduler orders your nodes!
🍿 BPVM Snack Pack Series
- ← #11: Linking and Binding
- #12: Statements 101 ← You are here
- #13: The DAG Scheduler →
Cite this post
Jayden Zhang. “BPVM Snack Pack #12 - Statements 101: The Language Before Bytecode.” Jayden Zhang’s Blog, October 28, 2025. https://www.jaydengames.com/posts/bpvm-snack-12-statements/
BibTeX
@misc{bpvm-snack-12-statements2025,
author = {Jayden Zhang},
title = {BPVM Snack Pack #12 - Statements 101: The Language Before Bytecode},
year = {2025},
url = {https://www.jaydengames.com/posts/bpvm-snack-12-statements/},
note = {Jayden Zhang’s Blog}
}