Deep Understanding Google Blockly - Part 3: Code Generation
Deep Understanding Google Blockly - Part 3: Code Generation
A comprehensive technical deep-dive into Blockly's code generation system
Table of Contents
- Introduction
- Code Generation Architecture
- The CodeGenerator Base Class
- Block-to-Code Translation
- Operator Precedence
- Language-Specific Generators
- Writing Custom Generators
- Advanced Code Generation
Introduction
One of Blockly's most powerful features is its ability to generate executable code in multiple programming languages from the same visual blocks. This language-agnostic approach means:
- Write once, generate anywhere - Same blocks → JavaScript, Python, Dart, Lua, PHP
- Type-safe generation - Proper operator precedence and type handling
- Customizable output - Add prefixes, suffixes, infinite loop traps
- Human-readable code - Generated code follows language conventions
This article explores the sophisticated code generation system that makes this possible.
Code Generation Architecture
High-Level Flow
┌─────────────────────────────────────────┐
│ Workspace with Blocks │
│ (Visual representation) │
└─────────────────────────────────────────┘
↓
Generator.workspaceToCode()
↓
┌─────────────────────────────────────────┐
│ 1. GET TOP-LEVEL BLOCKS │
│ workspace.getTopBlocks(true) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. INITIALIZE GENERATOR │
│ generator.init(workspace) │
│ - Create name database │
│ - Reset definitions │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. GENERATE CODE FOR EACH BLOCK │
│ for each block: │
│ generator.blockToCode(block) │
│ - Recursive depth-first │
│ - Returns code string │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 4. ASSEMBLE FINAL CODE │
│ - Join code blocks │
│ - Add definitions/imports │
│ - Apply finish() cleanup │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Executable Code String │
│ (JavaScript, Python, etc.) │
└─────────────────────────────────────────┘
Key Components
Generator System (/generators/):
javascript/- JavaScript code generatorpython/- Python code generatordart/- Dart code generatorlua/- Lua code generatorphp/- PHP code generator
Core Generator (core/generator.ts):
CodeGenerator- Base class for all generatorsNames- Variable/function name management- Order constants - Operator precedence definitions
The CodeGenerator Base Class
Core Structure
From core/generator.ts:
export class CodeGenerator {
name_: string;
// Block generator functions
forBlock: Record<
string,
(block: Block, generator: this) => [string, number] | string | null
> = Object.create(null);
// Code injection
INFINITE_LOOP_TRAP: string | null = null;
STATEMENT_PREFIX: string | null = null;
STATEMENT_SUFFIX: string | null = null;
// Formatting
INDENT = ' ';
COMMENT_WRAP = 60;
// State
protected definitions_: {[key: string]: string} = Object.create(null);
protected functionNames_: {[key: string]: string} = Object.create(null);
nameDB_?: Names = undefined;
constructor(name: string) {
this.name_ = name;
}
workspaceToCode(workspace?: Workspace): string {
if (!workspace) {
workspace = common.getMainWorkspace();
}
const code = [];
this.init(workspace);
const blocks = workspace.getTopBlocks(true);
for (let i = 0; i < blocks.length; i++) {
let line = this.blockToCode(blocks[i]);
if (Array.isArray(line)) {
// Value blocks return [code, precedence]
line = line[0];
}
if (line) {
if (blocks[i].outputConnection) {
// Naked value - apply scrubber
line = this.scrubNakedValue(line);
// Add statement prefix/suffix
if (this.STATEMENT_PREFIX && !blocks[i].suppressPrefixSuffix) {
line = this.injectId(this.STATEMENT_PREFIX, blocks[i]) + line;
}
if (this.STATEMENT_SUFFIX && !blocks[i].suppressPrefixSuffix) {
line = line + this.injectId(this.STATEMENT_SUFFIX, blocks[i]);
}
}
code.push(line);
}
}
let codeString = code.join('\n');
codeString = this.finish(codeString);
// Clean whitespace
codeString = codeString.replace(/^\s+\n/, '');
codeString = codeString.replace(/\n\s+$/, '\n');
codeString = codeString.replace(/[ \t]+\n/g, '\n');
return codeString;
}
}
Initialization
init(workspace: Workspace) {
// Create name database
if (!this.nameDB_) {
this.nameDB_ = new Names(this.RESERVED_WORDS_);
} else {
this.nameDB_.reset();
}
// Populate with variable names
const variables = workspace.getAllVariables();
for (let i = 0; i < variables.length; i++) {
const variable = variables[i];
this.nameDB_.getName(variable.getId(), NameType.VARIABLE);
}
// Reset definitions
this.definitions_ = Object.create(null);
this.functionNames_ = Object.create(null);
// Reset variable DB (for procedures)
if (this.nameDB_.populateProcedures) {
this.nameDB_.populateProcedures(workspace);
}
}
Block-to-Code Translation
The blockToCode Method
blockToCode(block: Block | null, opt_thisOnly?: boolean): string | [string, number] {
if (!block) {
return '';
}
if (!block.isEnabled()) {
// Skip disabled blocks, move to next
return opt_thisOnly ? '' : this.blockToCode(block.getNextBlock());
}
if (block.isInsertionMarker()) {
// Skip insertion markers
return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]);
}
// Look up generator function
const func = this.forBlock[block.type];
if (typeof func !== 'function') {
throw Error(
`${this.name_} generator does not know how to generate code ` +
`for block type "${block.type}".`
);
}
// Call generator function
let code = func.call(block, block, this);
if (Array.isArray(code)) {
// Value blocks return [code, order]
return [this.scrub_(block, code[0], opt_thisOnly), code[1]];
} else if (typeof code === 'string') {
// Statement blocks return code string
// Add statement prefix/suffix
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
code = this.injectId(this.STATEMENT_PREFIX, block) + code;
}
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
code = code + this.injectId(this.STATEMENT_SUFFIX, block);
}
return this.scrub_(block, code, opt_thisOnly);
} else if (code === null) {
// Block handled code generation itself
return '';
}
throw SyntaxError('Invalid code generated: ' + code);
}
Helper Methods
Getting Input Values:
valueToCode(block: Block, name: string, outerOrder: number): string {
const targetBlock = block.getInputTargetBlock(name);
if (!targetBlock) {
return '';
}
const tuple = this.blockToCode(targetBlock);
if (!Array.isArray(tuple)) {
// Statement block in value input - shouldn't happen
return '';
}
let code = tuple[0];
const innerOrder = tuple[1];
if (!code) {
return '';
}
// Add parentheses if necessary
if (innerOrder !== undefined && outerOrder !== undefined) {
if (this.shouldAddParentheses_(innerOrder, outerOrder)) {
code = '(' + code + ')';
}
}
return code;
}
Getting Statement Code:
statementToCode(block: Block, name: string): string {
const targetBlock = block.getInputTargetBlock(name);
let code = this.blockToCode(targetBlock);
if (Array.isArray(code)) {
// Value block in statement input - just use code
code = code[0];
}
// Apply infinite loop trap if configured
if (this.INFINITE_LOOP_TRAP) {
code = this.injectId(this.INFINITE_LOOP_TRAP, block) + code;
}
return code;
}
Prefix Lines:
prefixLines(text: string, prefix: string): string {
return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix);
}
Operator Precedence
Why Precedence Matters
Consider this expression: 5 + 3 * 2
Without proper precedence handling:
var x = 5 + 3 * 2; // Correct: 11
var y = (5 + 3) * 2; // Wrong if we always add parentheses: 16
Blockly's generator handles this automatically.
Order Constants
From generators/javascript/javascript_generator.ts:
export enum Order {
ATOMIC = 0, // 0 "" ...
NEW = 1.1, // new
MEMBER = 1.2, // . []
FUNCTION_CALL = 2, // ()
INCREMENT = 3, // ++
DECREMENT = 3, // --
BITWISE_NOT = 4.1, // ~
UNARY_PLUS = 4.2, // +
UNARY_NEGATION = 4.3, // -
LOGICAL_NOT = 4.4, // !
TYPEOF = 4.5, // typeof
VOID = 4.6, // void
DELETE = 4.7, // delete
AWAIT = 4.8, // await
EXPONENTIATION = 5.0, // **
MULTIPLICATION = 5.1, // *
DIVISION = 5.2, // /
MODULUS = 5.3, // %
SUBTRACTION = 6.1, // -
ADDITION = 6.2, // +
BITWISE_SHIFT = 7, // << >> >>>
RELATIONAL = 8, // < <= > >=
IN = 8, // in
INSTANCEOF = 8, // instanceof
EQUALITY = 9, // == != === !==
BITWISE_AND = 10, // &
BITWISE_XOR = 11, // ^
BITWISE_OR = 12, // |
LOGICAL_AND = 13, // &&
LOGICAL_OR = 14, // ||
CONDITIONAL = 15, // ?:
ASSIGNMENT = 16, // = += -= **= *= /= %= <<= >>= ...
YIELD = 17, // yield
COMMA = 18, // ,
NONE = 99, // (...)
}
Using Precedence
Math Addition Example:
// generators/javascript/math.ts
export function math_arithmetic(
block: Block,
generator: JavascriptGenerator,
) {
const OPERATORS: {[key: string]: [string, Order]} = {
'ADD': [' + ', Order.ADDITION],
'MINUS': [' - ', Order.SUBTRACTION],
'MULTIPLY': [' * ', Order.MULTIPLICATION],
'DIVIDE': [' / ', Order.DIVISION],
'POWER': [' ** ', Order.EXPONENTIATION],
};
const tuple = OPERATORS[block.getFieldValue('OP')];
const operator = tuple[0];
const order = tuple[1];
const argument0 = generator.valueToCode(block, 'A', order) || '0';
const argument1 = generator.valueToCode(block, 'B', order) || '0';
const code = argument0 + operator + argument1;
return [code, order];
}
Comparison Example:
export function logic_compare(
block: Block,
generator: JavascriptGenerator,
) {
const OPERATORS: {[key: string]: string} = {
'EQ': '==',
'NEQ': '!=',
'LT': '<',
'LTE': '<=',
'GT': '>',
'GTE': '>=',
};
const operator = OPERATORS[block.getFieldValue('OP')];
const order = Order.RELATIONAL;
const argument0 = generator.valueToCode(block, 'A', order) || '0';
const argument1 = generator.valueToCode(block, 'B', order) || '0';
const code = argument0 + ' ' + operator + ' ' + argument1;
return [code, order];
}
Language-Specific Generators
JavaScript Generator
From generators/javascript/javascript_generator.ts:
export class JavascriptGenerator extends CodeGenerator {
constructor() {
super('JavaScript');
}
init(workspace: Workspace) {
super.init(workspace);
// Add common definitions
if (!this.nameDB_.getDistinctName) {
this.nameDB_!.getDistinctName = this.getDistinctName;
}
}
finish(code: string): string {
// Convert definitions to code
const definitions = [];
for (const name in this.definitions_) {
definitions.push(this.definitions_[name]);
}
// Add definitions before code
const allDefs = definitions.join('\n\n');
return allDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n\n') + code;
}
scrubNakedValue(line: string): string {
return line + ';\n';
}
scrub_(block: Block, code: string, opt_thisOnly?: boolean): string {
let commentCode = '';
// Get comment from block
if (!block.outputConnection || !opt_thisOnly) {
const comment = block.getCommentText();
if (comment) {
commentCode += this.prefixLines(comment, '// ') + '\n';
}
// Collect comments from all blocks in stack
for (let i = 0; i < block.inputList.length; i++) {
const input = block.inputList[i];
if (input.type === inputTypes.STATEMENT) {
const childBlock = input.connection!.targetBlock();
if (childBlock) {
const childComments = this.allNestedComments(childBlock);
if (childComments) {
commentCode += this.prefixLines(childComments, '// ');
}
}
}
}
}
const nextBlock = block.nextConnection?.targetBlock();
const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock);
return commentCode + code + nextCode;
}
}
Python Generator Differences
From generators/python/python_generator.ts:
export class PythonGenerator extends CodeGenerator {
constructor() {
super('Python');
// Python-specific settings
this.INDENT = ' ';
}
init(workspace: Workspace) {
super.init(workspace);
// Initialize import dictionary
this.imports_ = Object.create(null);
}
finish(code: string): string {
// Convert imports
const imports = [];
for (const name in this.imports_) {
imports.push(this.imports_[name]);
}
// Convert definitions
const definitions = [];
for (const name in this.definitions_) {
definitions.push(this.definitions_[name]);
}
// Assemble: imports, then definitions, then code
const allDefs = imports.join('\n') + '\n\n' +
definitions.join('\n\n');
return allDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n') + code;
}
scrubNakedValue(line: string): string {
// Python doesn't need semicolons
return line + '\n';
}
}
Example: Loop Generator Comparison
JavaScript:
export function controls_repeat_ext(
block: Block,
generator: JavascriptGenerator,
) {
const repeats = generator.valueToCode(block, 'TIMES', Order.ASSIGNMENT) || '0';
let branch = generator.statementToCode(block, 'DO');
branch = generator.addLoopTrap(branch, block);
const loopVar = generator.nameDB_!.getDistinctName('count', NameType.VARIABLE);
const code =
'for (var ' + loopVar + ' = 0; ' +
loopVar + ' < ' + repeats + '; ' +
loopVar + '++) {\n' +
branch + '}\n';
return code;
}
Python:
export function controls_repeat_ext(
block: Block,
generator: PythonGenerator,
) {
const repeats = generator.valueToCode(block, 'TIMES', Order.NONE) || '0';
let branch = generator.statementToCode(block, 'DO');
branch = generator.addLoopTrap(branch, block) || generator.PASS;
const loopVar = generator.nameDB_!.getDistinctName('count', NameType.VARIABLE);
const code =
'for ' + loopVar + ' in range(' + repeats + '):\n' +
branch;
return code;
}
Writing Custom Generators
Basic Pattern
// 1. Register the generator function
javascriptGenerator.forBlock['custom_block'] = function(block, generator) {
// 2. Get field values
const fieldValue = block.getFieldValue('FIELD_NAME');
// 3. Get input values
const inputValue = generator.valueToCode(
block,
'INPUT_NAME',
Order.ATOMIC
) || 'defaultValue';
// 4. Get statement code
const statementCode = generator.statementToCode(block, 'STATEMENT_NAME');
// 5. Generate code
const code = `customFunction(${inputValue})`;
// 6. Return based on block type
// For value blocks: return [code, order]
// For statement blocks: return code
return [code, Order.FUNCTION_CALL];
};
Example: Custom Math Block
Block Definition:
Blockly.Blocks['math_custom_operation'] = {
init: function() {
this.appendValueInput('A')
.setCheck('Number')
.appendField('calculate');
this.appendDummyInput()
.appendField(new Blockly.FieldDropdown([
['double', 'DOUBLE'],
['square', 'SQUARE'],
['negate', 'NEGATE']
]), 'OP');
this.setOutput(true, 'Number');
this.setColour(230);
}
};
JavaScript Generator:
javascriptGenerator.forBlock['math_custom_operation'] = function(block, generator) {
const operation = block.getFieldValue('OP');
const value = generator.valueToCode(block, 'A', Order.ATOMIC) || '0';
let code;
switch (operation) {
case 'DOUBLE':
code = value + ' * 2';
return [code, Order.MULTIPLICATION];
case 'SQUARE':
code = 'Math.pow(' + value + ', 2)';
return [code, Order.FUNCTION_CALL];
case 'NEGATE':
code = '-' + value;
return [code, Order.UNARY_NEGATION];
}
};
Python Generator:
pythonGenerator.forBlock['math_custom_operation'] = function(block, generator) {
const operation = block.getFieldValue('OP');
const value = generator.valueToCode(block, 'A', Order.ATOMIC) || '0';
let code;
switch (operation) {
case 'DOUBLE':
code = value + ' * 2';
return [code, Order.MULTIPLICATIVE];
case 'SQUARE':
code = value + ' ** 2';
return [code, Order.EXPONENTIATION];
case 'NEGATE':
code = '-' + value;
return [code, Order.UNARY_SIGN];
}
};
Advanced Code Generation
Adding Function Definitions
javascriptGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) {
const funcName = generator.getVariableName(block.getFieldValue('NAME'));
const args = [];
for (let i = 0; i < block.arguments_.length; i++) {
args[i] = generator.valueToCode(block, 'ARG' + i, Order.NONE) || 'null';
}
const code = funcName + '(' + args.join(', ') + ');\n';
return code;
};
javascriptGenerator.forBlock['procedures_defnoreturn'] = function(block, generator) {
const funcName = generator.getVariableName(block.getFieldValue('NAME'));
const branch = generator.statementToCode(block, 'STACK') || '';
const args = block.arguments_.map(arg =>
generator.getVariableName(arg)
).join(', ');
// Add to definitions instead of inline code
const code =
'function ' + funcName + '(' + args + ') {\n' +
branch +
'}';
generator.definitions_[funcName] = code;
return null; // Definition blocks don't generate inline code
};
Loop Traps
Prevent infinite loops in generated code:
const workspace = Blockly.inject('blocklyDiv', {
// ...
});
// Set infinite loop trap
javascriptGenerator.INFINITE_LOOP_TRAP =
'if (--window.LoopTrap == 0) throw "Infinite loop.";\n';
// Before running code:
window.LoopTrap = 1000; // Max iterations
eval(code);
Statement Prefixes
Highlight blocks during execution:
javascriptGenerator.STATEMENT_PREFIX = 'highlightBlock(%1);\n';
function highlightBlock(id) {
workspace.highlightBlock(id);
}
// Generated code includes:
highlightBlock('block_id_123');
doSomething();
Summary
In Part 3, we've explored:
✅ Code Generation Architecture - Workspace to executable code flow
✅ CodeGenerator Class - Base class and core methods
✅ Block Translation - blockToCode and helper methods
✅ Operator Precedence - Proper parenthesization
✅ Language Generators - JavaScript, Python, and others
✅ Custom Generators - Writing your own code generators
✅ Advanced Features - Definitions, loop traps, statement prefixes
In Part 4, we'll explore advanced topics including the event system, serialization, connection management, and plugin architecture.
This article is based on analysis of Blockly v12.3.1 source code. For the latest information, refer to the official Blockly documentation.