fsqlite_types/opcode.rs
1/// Read the top-N bound for `SorterOpen` from register P3 instead of treating
2/// P3 as an immediate integer.
3pub const SORTER_OPEN_TOP_N_REGISTER: u16 = 0x0001;
4
5/// Make `SorterCompare` treat P3 as a candidate sort-key record and jump to P2
6/// when a bounded sorter would reject that candidate.
7///
8/// In this mode P3 is consumed: after either the P2 jump or fallthrough, the
9/// register is dead and its contents are undefined. Bytecode must rewrite P3
10/// before any later read.
11pub const SORTER_COMPARE_TOP_N_PREFLIGHT: u16 = 0x0001;
12
13/// VDBE (Virtual Database Engine) opcodes.
14///
15/// These correspond 1:1 to the upstream SQLite VDBE opcode set. Each opcode
16/// represents a single operation in the bytecode program that the VDBE
17/// executes. Opcodes are numbered sequentially; the specific numeric values
18/// match C SQLite for debugging/comparison purposes.
19///
20/// Reference: canonical upstream SQLite opcode definitions.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[repr(u8)]
23#[allow(clippy::enum_variant_names)]
24pub enum Opcode {
25 // === Control Flow ===
26 /// Jump to address P2.
27 Goto = 1,
28 /// Push return address, jump to P2.
29 Gosub = 2,
30 /// Pop return address, jump to it.
31 Return = 3,
32 /// Initialize coroutine. P1=coroutine reg, P2=jump on first entry.
33 InitCoroutine = 4,
34 /// End coroutine, jump to return address.
35 EndCoroutine = 5,
36 /// Yield control to/from coroutine.
37 Yield = 6,
38 /// Halt if register P3 is NULL.
39 HaltIfNull = 7,
40 /// Halt execution (with optional error).
41 Halt = 8,
42
43 // === Constants & Values ===
44 /// Set register P2 to integer value P1.
45 Integer = 9,
46 /// Set register P2 to 64-bit integer from P4.
47 Int64 = 10,
48 /// Set register P2 to real value from P4.
49 Real = 11,
50 /// Set register P2 to string P4 (zero-terminated).
51 String8 = 12,
52 /// Set register P2 to string of length P1 from P4.
53 String = 13,
54 /// Begin subroutine / set register P2 to NULL.
55 BeginSubrtn = 14,
56 /// Set registers P2..P2+P3-1 to NULL.
57 Null = 15,
58 /// Set register to soft NULL (for optimization).
59 SoftNull = 16,
60 /// Set register P2 to blob of length P1 from P4.
61 Blob = 17,
62 /// Set register P2 to the value of variable/parameter P1.
63 Variable = 18,
64
65 // === Register Operations ===
66 /// Move P3 registers from P1 to P2.
67 Move = 19,
68 /// Copy register P1 to P2 (and optionally more).
69 Copy = 20,
70 /// Shallow copy register P1 to P2.
71 SCopy = 21,
72 /// Copy integer value from P1 to P2.
73 IntCopy = 22,
74
75 // === Foreign Key ===
76 /// Check foreign key constraints.
77 FkCheck = 23,
78
79 // === Result ===
80 /// Output a row of P2 registers starting at P1.
81 ResultRow = 24,
82
83 // === Arithmetic & String ===
84 /// Concatenate P1 and P2, store in P3.
85 Concat = 25,
86 /// P3 = P2 + P1.
87 Add = 26,
88 /// P3 = P2 - P1.
89 Subtract = 27,
90 /// P3 = P2 * P1.
91 Multiply = 28,
92 /// P3 = P2 / P1.
93 Divide = 29,
94 /// P3 = P2 % P1.
95 Remainder = 30,
96
97 // === Collation ===
98 /// Set collation sequence for comparison.
99 CollSeq = 31,
100
101 // === Bitwise ===
102 /// P3 = P1 & P2.
103 BitAnd = 32,
104 /// P3 = P1 | P2.
105 BitOr = 33,
106 /// P3 = P2 << P1.
107 ShiftLeft = 34,
108 /// P3 = P2 >> P1.
109 ShiftRight = 35,
110
111 // === Type Conversion ===
112 /// Add integer P2 to register P1.
113 AddImm = 36,
114 /// Fail if P1 is not an integer; optionally jump to P2.
115 MustBeInt = 37,
116 /// If P1 is integer, convert to real in-place.
117 RealAffinity = 38,
118 /// Cast register P1 to type P2.
119 Cast = 39,
120
121 // === Comparison ===
122 /// Jump to P2 if P1 == P3.
123 Eq = 40,
124 /// Jump to P2 if P1 != P3.
125 Ne = 41,
126 /// Jump to P2 if P3 < P1.
127 Lt = 42,
128 /// Jump to P2 if P3 <= P1.
129 Le = 43,
130 /// Jump to P2 if P3 > P1.
131 Gt = 44,
132 /// Jump to P2 if P3 >= P1.
133 Ge = 45,
134 /// Jump if the previous comparison was Eq (for multi-column indexes).
135 ElseEq = 46,
136
137 // === Permutation & Compare ===
138 /// Set up permutation for subsequent Compare.
139 Permutation = 47,
140 /// Compare P1..P1+P3-1 with P2..P2+P3-1.
141 Compare = 48,
142
143 // === Branching ===
144 /// Jump to one of P1, P2, or P3 based on comparison result.
145 Jump = 49,
146 /// P3 = P1 AND P2 (three-valued logic).
147 And = 50,
148 /// P3 = P1 OR P2 (three-valued logic).
149 Or = 51,
150 /// Apply IS TRUE test.
151 IsTrue = 52,
152 /// P2 = NOT P1.
153 Not = 53,
154 /// P2 = ~P1 (bitwise not).
155 BitNot = 54,
156 /// Jump to P2 on first execution only.
157 Once = 55,
158 /// Jump to P2 if P1 is true (non-zero and non-NULL).
159 If = 56,
160 /// Jump to P2 if P1 is false (zero or NULL).
161 IfNot = 57,
162 /// Jump to P2 if P1 is NULL.
163 IsNull = 58,
164 /// Type check against P5 type mask; jump to P2 on mismatch.
165 IsType = 59,
166 /// P2 = 0 if any of P1, P2, P3 is NULL.
167 ZeroOrNull = 60,
168 /// Jump to P2 if P1 is not NULL.
169 NotNull = 61,
170 /// Jump to P2 if the current row of cursor P1 is NULL.
171 IfNullRow = 62,
172
173 // === Column Access ===
174 /// Extract byte offset of cursor.
175 Offset = 63,
176 /// Extract column P2 from cursor P1 into register P3.
177 Column = 64,
178 /// Type-check columns against declared types.
179 TypeCheck = 65,
180 /// Apply type affinity to P2 registers starting at P1.
181 Affinity = 66,
182
183 // === Record Building ===
184 /// Build a record from P1..P1+P2-1 registers into P3.
185 MakeRecord = 67,
186
187 // === Counting ===
188 /// Store the number of rows in cursor P1 into register P2.
189 Count = 68,
190
191 // === Transaction Control ===
192 /// Begin, release, or rollback a savepoint.
193 Savepoint = 69,
194 /// Set or clear auto-commit mode.
195 AutoCommit = 70,
196 /// Begin a transaction on database P1.
197 Transaction = 71,
198
199 // === Cookie Access ===
200 /// Read database cookie P3 from database P1 into register P2.
201 ReadCookie = 72,
202 /// Write P3 to database cookie P2 of database P1.
203 SetCookie = 73,
204
205 // === Cursor Operations ===
206 /// Reopen an index cursor (P1) if it's on a different root page.
207 ReopenIdx = 74,
208 /// Open a read cursor on table/index P2 in database P3.
209 OpenRead = 75,
210 /// Open a write cursor on table/index P2 in database P3.
211 OpenWrite = 76,
212 /// Open cursor P1 as a duplicate of cursor P2.
213 OpenDup = 77,
214 /// Open an ephemeral (temporary) table cursor.
215 OpenEphemeral = 78,
216 /// Open an auto-index ephemeral cursor.
217 OpenAutoindex = 79,
218 /// Open a sorter cursor.
219 SorterOpen = 80,
220 /// Test if sequence number has been used.
221 SequenceTest = 81,
222 /// Open a pseudo-table cursor (reads from a register).
223 OpenPseudo = 82,
224 /// Close cursor P1.
225 Close = 83,
226 /// Set the columns-used mask for cursor P1.
227 ColumnsUsed = 84,
228
229 // === Seek Operations ===
230 /// Seek cursor P1 to the largest entry less than P3.
231 SeekLT = 85,
232 /// Seek cursor P1 to the largest entry <= P3.
233 SeekLE = 86,
234 /// Seek cursor P1 to the smallest entry >= P3.
235 SeekGE = 87,
236 /// Seek cursor P1 to the smallest entry greater than P3.
237 SeekGT = 88,
238 /// Optimized seek-scan for small result sets.
239 SeekScan = 89,
240 /// Mark seek hit range for covering index optimization.
241 SeekHit = 90,
242 /// Jump to P2 if cursor P1 is not open.
243 IfNotOpen = 91,
244
245 // === Index Lookup ===
246 /// Like NotFound but with Bloom filter check.
247 IfNoHope = 92,
248 /// Jump to P2 if key P3 is NOT found (no conflict).
249 NoConflict = 93,
250 /// Jump to P2 if key P3 is NOT found in cursor P1.
251 NotFound = 94,
252 /// Jump to P2 if key P3 IS found in cursor P1.
253 Found = 95,
254
255 // === Rowid Seek ===
256 /// Seek cursor P1 to rowid P3; jump to P2 if not found.
257 SeekRowid = 96,
258 /// Jump to P2 if rowid P3 does NOT exist in cursor P1.
259 NotExists = 97,
260
261 // === Sequence & Rowid ===
262 /// Store next sequence value for cursor P1 into register P2.
263 Sequence = 98,
264 /// Generate a new unique rowid for cursor P1.
265 NewRowid = 99,
266
267 // === Insert & Delete ===
268 /// Insert record from P2 with rowid P3 into cursor P1.
269 Insert = 100,
270 /// Copy a cell directly from one cursor to another.
271 RowCell = 101,
272 /// Delete the current row of cursor P1.
273 Delete = 102,
274 /// Reset the change counter.
275 ResetCount = 103,
276
277 // === Sorter Operations ===
278 /// Compare a sorter key.
279 ///
280 /// With `SORTER_COMPARE_TOP_N_PREFLIGHT`, P3 is a packed candidate-key
281 /// record and execution jumps to P2 when the bounded sorter is full and
282 /// the candidate cannot displace its current worst row.
283 /// The preflight form consumes P3; it is dead and undefined after either
284 /// the jump or fallthrough and must be rewritten before any later read.
285 SorterCompare = 104,
286 /// Read data from the sorter.
287 SorterData = 105,
288
289 // === Row Data ===
290 /// Copy the complete row data of cursor P1 into register P2.
291 RowData = 106,
292 /// Store the rowid of cursor P1 into register P2.
293 Rowid = 107,
294 /// Set cursor P1 to a NULL row.
295 NullRow = 108,
296
297 // === Cursor Navigation ===
298 /// Seek to end of table (no-op for reading, positions for append).
299 SeekEnd = 109,
300 /// Move cursor P1 to the last entry; jump to P2 if empty.
301 Last = 110,
302 /// Jump to P2 if table size is between P3 and P4.
303 IfSizeBetween = 111,
304 /// Sort (alias for SorterSort in some contexts).
305 SorterSort = 112,
306 /// Sort cursor P1.
307 Sort = 113,
308 /// Rewind cursor P1 to the first entry; jump to P2 if empty.
309 Rewind = 114,
310 /// Jump to P2 if cursor P1's table is empty.
311 IfEmpty = 115,
312
313 // === Iteration ===
314 /// Advance sorter to next entry.
315 SorterNext = 116,
316 /// Move cursor P1 to the previous entry; jump to P2 if done.
317 Prev = 117,
318 /// Move cursor P1 to the next entry; jump to P2 if done.
319 Next = 118,
320
321 // === Index Insert/Delete ===
322 /// Insert record P2 into index cursor P1.
323 IdxInsert = 119,
324 /// Insert into sorter.
325 SorterInsert = 120,
326 /// Delete from index cursor P1.
327 IdxDelete = 121,
328
329 // === Deferred Seek ===
330 /// Defer a seek on cursor P1 using the rowid from index cursor P2.
331 DeferredSeek = 122,
332 /// Extract rowid from index entry of cursor P1.
333 IdxRowid = 123,
334 /// Complete a previously deferred seek.
335 FinishSeek = 124,
336
337 // === Index Comparison ===
338 /// Jump to P2 if index key of P1 <= key.
339 IdxLE = 125,
340 /// Jump to P2 if index key of P1 > key.
341 IdxGT = 126,
342 /// Jump to P2 if index key of P1 < key.
343 IdxLT = 127,
344 /// Jump to P2 if index key of P1 >= key.
345 IdxGE = 128,
346
347 // === DDL Operations ===
348 /// Destroy (drop) a B-tree rooted at page P1.
349 Destroy = 129,
350 /// Clear (delete all rows from) a table or index.
351 Clear = 130,
352 /// Reset a sorter cursor.
353 ResetSorter = 131,
354 /// Allocate a new B-tree, store root page number in P2.
355 CreateBtree = 132,
356
357 // === Schema Operations ===
358 /// Execute an SQL statement stored in P4.
359 SqlExec = 133,
360 /// Parse the schema for database P1.
361 ParseSchema = 134,
362 /// Load analysis data for database P1.
363 LoadAnalysis = 135,
364 /// Drop a table.
365 DropTable = 136,
366 /// Drop an index.
367 DropIndex = 137,
368 /// Drop a trigger.
369 DropTrigger = 138,
370
371 // === Integrity Check ===
372 /// Run integrity check on database P1.
373 IntegrityCk = 139,
374
375 // === RowSet Operations ===
376 /// Add integer P2 to rowset P1.
377 RowSetAdd = 140,
378 /// Read next value from rowset P1 into P3; jump to P2 when empty.
379 RowSetRead = 141,
380 /// Test if P3 exists in rowset P1; jump to P2 if found.
381 RowSetTest = 142,
382
383 // === Trigger/Program ===
384 /// Call a trigger sub-program.
385 Program = 143,
386 /// Copy trigger parameter into register P2.
387 Param = 144,
388
389 // === FK Counters ===
390 /// Increment or decrement FK counter.
391 FkCounter = 145,
392 /// Jump to P2 if FK counter is zero.
393 FkIfZero = 146,
394
395 // === Memory/Counter ===
396 /// Set register P2 to max of P2 and register P1.
397 MemMax = 147,
398
399 // === Conditional Jumps ===
400 /// Jump to P2 if register P1 > 0; decrement by P3.
401 IfPos = 148,
402 /// Compute offset limit.
403 OffsetLimit = 149,
404 /// Jump to P2 if register P1 is not zero.
405 IfNotZero = 150,
406 /// Decrement P1, jump to P2 if result is zero.
407 DecrJumpZero = 151,
408
409 // === Aggregate Functions ===
410 /// Invoke aggregate inverse function.
411 AggInverse = 152,
412 /// Invoke aggregate step function.
413 AggStep = 153,
414 /// Step variant with different init semantics.
415 AggStep1 = 154,
416 /// Extract aggregate intermediate value.
417 AggValue = 155,
418 /// Finalize aggregate function.
419 AggFinal = 156,
420
421 // === WAL & Journal ===
422 /// Checkpoint the WAL for database P1.
423 Checkpoint = 157,
424 /// Set journal mode for database P1.
425 JournalMode = 158,
426
427 // === Vacuum ===
428 /// Vacuum the database.
429 Vacuum = 159,
430 /// Incremental vacuum step; jump to P2 if done.
431 IncrVacuum = 160,
432
433 // === Expiry & Locking ===
434 /// Mark prepared statement as expired.
435 Expire = 161,
436 /// Lock cursor P1.
437 CursorLock = 162,
438 /// Unlock cursor P1.
439 CursorUnlock = 163,
440 /// Lock table P2 in database P1.
441 TableLock = 164,
442
443 // === Virtual Table ===
444 /// Begin a virtual table transaction.
445 VBegin = 165,
446 /// Create a virtual table.
447 VCreate = 166,
448 /// Destroy a virtual table.
449 VDestroy = 167,
450 /// Open a virtual table cursor.
451 VOpen = 168,
452 /// Check virtual table integrity.
453 VCheck = 169,
454 /// Initialize IN constraint for virtual table.
455 VInitIn = 170,
456 /// Apply filter to virtual table cursor.
457 VFilter = 171,
458 /// Read column from virtual table cursor.
459 VColumn = 172,
460 /// Advance virtual table cursor.
461 VNext = 173,
462 /// Rename a virtual table.
463 VRename = 174,
464 /// Update/insert/delete on virtual table.
465 VUpdate = 175,
466
467 // === Page Count ===
468 /// Store database page count in register P2.
469 Pagecount = 176,
470 /// Set or read max page count.
471 MaxPgcnt = 177,
472
473 // === Functions ===
474 /// Call a pure (deterministic) function.
475 PureFunc = 178,
476 /// Call a function (possibly with side effects).
477 Function = 179,
478
479 // === Subtype Operations ===
480 /// Clear the subtype from register P1.
481 ClrSubtype = 180,
482 /// Get subtype of P1 into P2.
483 GetSubtype = 181,
484 /// Set subtype of P2 from P1.
485 SetSubtype = 182,
486
487 // === Bloom Filter ===
488 /// Add entry to Bloom filter.
489 FilterAdd = 183,
490 /// Test Bloom filter; jump to P2 if definitely not present.
491 Filter = 184,
492
493 // === Trace & Init ===
494 /// Trace/profile callback.
495 Trace = 185,
496 /// Initialize VDBE program; jump to P2.
497 Init = 186,
498
499 // === Hints & Debug ===
500 /// Provide cursor hint to storage engine.
501 CursorHint = 187,
502 /// Mark that this program can be aborted.
503 Abortable = 188,
504 /// Release register range.
505 ReleaseReg = 189,
506
507 // === Time-travel (SQL:2011 temporal queries) ===
508 /// Set time-travel snapshot on cursor P1.
509 /// P4 carries `TimeTravelCommitSeq(n)` or `TimeTravelTimestamp(ts)`.
510 /// Must immediately follow the `OpenRead` for the same cursor.
511 /// The cursor becomes read-only; DML/DDL through it returns an error.
512 SetSnapshot = 190,
513
514 // === Noop & FrankenSQLite extensions ===
515 /// No operation.
516 Noop = 191,
517 /// Evaluate a literal-pattern LIKE fast path directly against a register.
518 LikeConstFast = 192,
519 /// Count a run of equal first-column index keys, advancing the cursor.
520 CountIndexEqRun = 193,
521
522 // === Superinstructions (bd-perf V2.1) ===
523 /// Fused NewRowid + MakeRecord + Insert for sequential append.
524 ///
525 /// P1 = cursor number
526 /// P2 = first register of column values (same as MakeRecord P1)
527 /// P3 = number of columns (same as MakeRecord P2)
528 /// P5 = Insert flags (OE_* conflict mode in low nibble)
529 ///
530 /// Combines three opcodes into one dispatch:
531 /// 1. Allocate next sequential rowid (using cached last_alloc_rowid)
532 /// 2. Serialize column registers into record blob
533 /// 3. Append to B-tree via table_insert (prechecked absent, append mode)
534 ///
535 /// Guard conditions (codegen must verify before emitting):
536 /// - No secondary indexes on the table
537 /// - No triggers
538 /// - No foreign keys
539 /// - Default ABORT conflict mode (OE_ABORT = 2 in low nibble)
540 /// - No generated/stored columns
541 FusedAppendInsert = 194,
542
543 /// Fused OpenWrite + Last for cursor setup in INSERT programs.
544 /// P1 = cursor, P2 = root page number, P3 = column count, P5 = flags.
545 /// Opens a write cursor and navigates to the last entry for append.
546 FusedOpenWriteLast = 195,
547
548 /// Fused `Integer(p1=lit, p2=reg) + ResultRow(p1=reg, p2=1)` pair.
549 ///
550 /// Emits a single-column result row whose only value is the literal
551 /// integer `p1`, then clears register `p2` (matching the post-`ResultRow`
552 /// side effect of `take_reg_range`, which drains the source register).
553 ///
554 /// P1 = integer literal value
555 /// P2 = source register (written with the literal, then consumed)
556 /// P3 = unused (reserved; must be 0)
557 /// P4 = `P4::None`
558 /// P5 = 0
559 ///
560 /// Correctness contract: byte-equivalent to the unfused pair. Only the
561 /// peephole codegen pass emits this opcode; it MUST verify that the
562 /// immediately-following `ResultRow` consumes exactly the register
563 /// written by `Integer` and outputs exactly one column.
564 FusedLiteralResultRow = 196,
565
566 /// Compute `SUBSTR(column, 1, P4)` directly from a table cursor column.
567 ///
568 /// P1 = cursor number, P2 = logical column index, P3 = output register,
569 /// P4 = `Int(prefix_len)`, P5 = 0.
570 ///
571 /// The engine may fast-path storage TEXT/BLOB payload prefixes without
572 /// materializing the full column. Unsupported storage classes fall back to
573 /// the equivalent scalar `substr(value, 1, prefix_len)` behavior.
574 ColumnSubstrPrefix = 197,
575
576 /// Compute `octet_length(column)` from a table cursor record header.
577 ///
578 /// P1 = cursor number, P2 = logical column index, P3 = output register,
579 /// P4 = `None`, P5 = 0.
580 ///
581 /// TEXT and BLOB byte lengths are encoded by the record serial type, so a
582 /// storage cursor can answer this without expanding overflow payloads or
583 /// allocating the source value. Unsupported storage classes fall back to
584 /// the equivalent scalar `octet_length(value)` behavior.
585 ColumnOctetLength = 198,
586
587 /// Jump if the most recent row-insert was suppressed by `OE_IGNORE`.
588 ///
589 /// P1 = unused, P2 = jump target. Branches to P2 when the engine's
590 /// `conflict_skip_idx` flag is set — i.e. the preceding `Insert`/`IdxInsert`
591 /// sequence for the current row hit a rowid or UNIQUE conflict under
592 /// `INSERT/UPDATE OR IGNORE` and was rolled back. Used by `UPDATE OR IGNORE
593 /// ... RETURNING` on rowid tables to skip the `RETURNING` emission for a row
594 /// that was ignored (GH #159), reusing the engine's exact conflict decision
595 /// rather than re-deriving uniqueness in codegen.
596 IfConflictSkip = 199,
597}
598
599impl Opcode {
600 /// Exclusive upper bound on valid opcode discriminants.
601 ///
602 /// Discriminants run `1..=199` (there is no zero opcode), so valid bytes
603 /// are exactly `1..COUNT` and the number of opcodes defined is `COUNT - 1`.
604 pub const COUNT: usize = 200;
605
606 /// Get the opcode name as a static string slice.
607 #[allow(clippy::too_many_lines)]
608 pub const fn name(self) -> &'static str {
609 match self {
610 Self::Goto => "Goto",
611 Self::Gosub => "Gosub",
612 Self::Return => "Return",
613 Self::InitCoroutine => "InitCoroutine",
614 Self::EndCoroutine => "EndCoroutine",
615 Self::Yield => "Yield",
616 Self::HaltIfNull => "HaltIfNull",
617 Self::Halt => "Halt",
618 Self::Integer => "Integer",
619 Self::Int64 => "Int64",
620 Self::Real => "Real",
621 Self::String8 => "String8",
622 Self::String => "String",
623 Self::BeginSubrtn => "BeginSubrtn",
624 Self::Null => "Null",
625 Self::SoftNull => "SoftNull",
626 Self::Blob => "Blob",
627 Self::Variable => "Variable",
628 Self::Move => "Move",
629 Self::Copy => "Copy",
630 Self::SCopy => "SCopy",
631 Self::IntCopy => "IntCopy",
632 Self::FkCheck => "FkCheck",
633 Self::ResultRow => "ResultRow",
634 Self::Concat => "Concat",
635 Self::Add => "Add",
636 Self::Subtract => "Subtract",
637 Self::Multiply => "Multiply",
638 Self::Divide => "Divide",
639 Self::Remainder => "Remainder",
640 Self::CollSeq => "CollSeq",
641 Self::BitAnd => "BitAnd",
642 Self::BitOr => "BitOr",
643 Self::ShiftLeft => "ShiftLeft",
644 Self::ShiftRight => "ShiftRight",
645 Self::AddImm => "AddImm",
646 Self::MustBeInt => "MustBeInt",
647 Self::RealAffinity => "RealAffinity",
648 Self::Cast => "Cast",
649 Self::Eq => "Eq",
650 Self::Ne => "Ne",
651 Self::Lt => "Lt",
652 Self::Le => "Le",
653 Self::Gt => "Gt",
654 Self::Ge => "Ge",
655 Self::ElseEq => "ElseEq",
656 Self::Permutation => "Permutation",
657 Self::Compare => "Compare",
658 Self::Jump => "Jump",
659 Self::And => "And",
660 Self::Or => "Or",
661 Self::IsTrue => "IsTrue",
662 Self::Not => "Not",
663 Self::BitNot => "BitNot",
664 Self::Once => "Once",
665 Self::If => "If",
666 Self::IfNot => "IfNot",
667 Self::IsNull => "IsNull",
668 Self::IsType => "IsType",
669 Self::ZeroOrNull => "ZeroOrNull",
670 Self::NotNull => "NotNull",
671 Self::IfNullRow => "IfNullRow",
672 Self::Offset => "Offset",
673 Self::Column => "Column",
674 Self::TypeCheck => "TypeCheck",
675 Self::Affinity => "Affinity",
676 Self::MakeRecord => "MakeRecord",
677 Self::Count => "Count",
678 Self::Savepoint => "Savepoint",
679 Self::AutoCommit => "AutoCommit",
680 Self::Transaction => "Transaction",
681 Self::ReadCookie => "ReadCookie",
682 Self::SetCookie => "SetCookie",
683 Self::ReopenIdx => "ReopenIdx",
684 Self::OpenRead => "OpenRead",
685 Self::OpenWrite => "OpenWrite",
686 Self::OpenDup => "OpenDup",
687 Self::OpenEphemeral => "OpenEphemeral",
688 Self::OpenAutoindex => "OpenAutoindex",
689 Self::SorterOpen => "SorterOpen",
690 Self::SequenceTest => "SequenceTest",
691 Self::OpenPseudo => "OpenPseudo",
692 Self::Close => "Close",
693 Self::ColumnsUsed => "ColumnsUsed",
694 Self::SeekLT => "SeekLT",
695 Self::SeekLE => "SeekLE",
696 Self::SeekGE => "SeekGE",
697 Self::SeekGT => "SeekGT",
698 Self::SeekScan => "SeekScan",
699 Self::SeekHit => "SeekHit",
700 Self::IfNotOpen => "IfNotOpen",
701 Self::IfNoHope => "IfNoHope",
702 Self::NoConflict => "NoConflict",
703 Self::NotFound => "NotFound",
704 Self::Found => "Found",
705 Self::SeekRowid => "SeekRowid",
706 Self::NotExists => "NotExists",
707 Self::Sequence => "Sequence",
708 Self::NewRowid => "NewRowid",
709 Self::Insert => "Insert",
710 Self::RowCell => "RowCell",
711 Self::Delete => "Delete",
712 Self::ResetCount => "ResetCount",
713 Self::SorterCompare => "SorterCompare",
714 Self::SorterData => "SorterData",
715 Self::RowData => "RowData",
716 Self::Rowid => "Rowid",
717 Self::NullRow => "NullRow",
718 Self::SeekEnd => "SeekEnd",
719 Self::Last => "Last",
720 Self::IfSizeBetween => "IfSizeBetween",
721 Self::SorterSort => "SorterSort",
722 Self::Sort => "Sort",
723 Self::Rewind => "Rewind",
724 Self::IfEmpty => "IfEmpty",
725 Self::SorterNext => "SorterNext",
726 Self::Prev => "Prev",
727 Self::Next => "Next",
728 Self::IdxInsert => "IdxInsert",
729 Self::SorterInsert => "SorterInsert",
730 Self::IdxDelete => "IdxDelete",
731 Self::DeferredSeek => "DeferredSeek",
732 Self::IdxRowid => "IdxRowid",
733 Self::FinishSeek => "FinishSeek",
734 Self::IdxLE => "IdxLE",
735 Self::IdxGT => "IdxGT",
736 Self::IdxLT => "IdxLT",
737 Self::IdxGE => "IdxGE",
738 Self::Destroy => "Destroy",
739 Self::Clear => "Clear",
740 Self::ResetSorter => "ResetSorter",
741 Self::CreateBtree => "CreateBtree",
742 Self::SqlExec => "SqlExec",
743 Self::ParseSchema => "ParseSchema",
744 Self::LoadAnalysis => "LoadAnalysis",
745 Self::DropTable => "DropTable",
746 Self::DropIndex => "DropIndex",
747 Self::DropTrigger => "DropTrigger",
748 Self::IntegrityCk => "IntegrityCk",
749 Self::RowSetAdd => "RowSetAdd",
750 Self::RowSetRead => "RowSetRead",
751 Self::RowSetTest => "RowSetTest",
752 Self::Program => "Program",
753 Self::Param => "Param",
754 Self::FkCounter => "FkCounter",
755 Self::FkIfZero => "FkIfZero",
756 Self::MemMax => "MemMax",
757 Self::IfPos => "IfPos",
758 Self::OffsetLimit => "OffsetLimit",
759 Self::IfNotZero => "IfNotZero",
760 Self::DecrJumpZero => "DecrJumpZero",
761 Self::AggInverse => "AggInverse",
762 Self::AggStep => "AggStep",
763 Self::AggStep1 => "AggStep1",
764 Self::AggValue => "AggValue",
765 Self::AggFinal => "AggFinal",
766 Self::Checkpoint => "Checkpoint",
767 Self::JournalMode => "JournalMode",
768 Self::Vacuum => "Vacuum",
769 Self::IncrVacuum => "IncrVacuum",
770 Self::Expire => "Expire",
771 Self::CursorLock => "CursorLock",
772 Self::CursorUnlock => "CursorUnlock",
773 Self::TableLock => "TableLock",
774 Self::VBegin => "VBegin",
775 Self::VCreate => "VCreate",
776 Self::VDestroy => "VDestroy",
777 Self::VOpen => "VOpen",
778 Self::VCheck => "VCheck",
779 Self::VInitIn => "VInitIn",
780 Self::VFilter => "VFilter",
781 Self::VColumn => "VColumn",
782 Self::VNext => "VNext",
783 Self::VRename => "VRename",
784 Self::VUpdate => "VUpdate",
785 Self::Pagecount => "Pagecount",
786 Self::MaxPgcnt => "MaxPgcnt",
787 Self::PureFunc => "PureFunc",
788 Self::Function => "Function",
789 Self::ClrSubtype => "ClrSubtype",
790 Self::GetSubtype => "GetSubtype",
791 Self::SetSubtype => "SetSubtype",
792 Self::FilterAdd => "FilterAdd",
793 Self::Filter => "Filter",
794 Self::Trace => "Trace",
795 Self::Init => "Init",
796 Self::CursorHint => "CursorHint",
797 Self::Abortable => "Abortable",
798 Self::ReleaseReg => "ReleaseReg",
799 Self::SetSnapshot => "SetSnapshot",
800 Self::Noop => "Noop",
801 Self::LikeConstFast => "LikeConstFast",
802 Self::CountIndexEqRun => "CountIndexEqRun",
803 Self::FusedAppendInsert => "FusedAppendInsert",
804 Self::FusedOpenWriteLast => "FusedOpenWriteLast",
805 Self::FusedLiteralResultRow => "FusedLiteralResultRow",
806 Self::ColumnSubstrPrefix => "ColumnSubstrPrefix",
807 Self::ColumnOctetLength => "ColumnOctetLength",
808 Self::IfConflictSkip => "IfConflictSkip",
809 }
810 }
811
812 /// Try to convert a u8 to an Opcode.
813 #[allow(clippy::too_many_lines)]
814 pub const fn from_byte(byte: u8) -> Option<Self> {
815 if byte == 0 || byte as usize >= Self::COUNT {
816 return None;
817 }
818 // SAFETY: All values 1..Opcode::COUNT are valid discriminants.
819 // We verified byte is in range above.
820 // Since the enum is repr(u8) with consecutive values, this is safe.
821 // However, since unsafe is forbidden, we use a match instead.
822 // For now, we accept the compile-time cost of a big match.
823 match byte {
824 1 => Some(Self::Goto),
825 2 => Some(Self::Gosub),
826 3 => Some(Self::Return),
827 4 => Some(Self::InitCoroutine),
828 5 => Some(Self::EndCoroutine),
829 6 => Some(Self::Yield),
830 7 => Some(Self::HaltIfNull),
831 8 => Some(Self::Halt),
832 9 => Some(Self::Integer),
833 10 => Some(Self::Int64),
834 11 => Some(Self::Real),
835 12 => Some(Self::String8),
836 13 => Some(Self::String),
837 14 => Some(Self::BeginSubrtn),
838 15 => Some(Self::Null),
839 16 => Some(Self::SoftNull),
840 17 => Some(Self::Blob),
841 18 => Some(Self::Variable),
842 19 => Some(Self::Move),
843 20 => Some(Self::Copy),
844 21 => Some(Self::SCopy),
845 22 => Some(Self::IntCopy),
846 23 => Some(Self::FkCheck),
847 24 => Some(Self::ResultRow),
848 25 => Some(Self::Concat),
849 26 => Some(Self::Add),
850 27 => Some(Self::Subtract),
851 28 => Some(Self::Multiply),
852 29 => Some(Self::Divide),
853 30 => Some(Self::Remainder),
854 31 => Some(Self::CollSeq),
855 32 => Some(Self::BitAnd),
856 33 => Some(Self::BitOr),
857 34 => Some(Self::ShiftLeft),
858 35 => Some(Self::ShiftRight),
859 36 => Some(Self::AddImm),
860 37 => Some(Self::MustBeInt),
861 38 => Some(Self::RealAffinity),
862 39 => Some(Self::Cast),
863 40 => Some(Self::Eq),
864 41 => Some(Self::Ne),
865 42 => Some(Self::Lt),
866 43 => Some(Self::Le),
867 44 => Some(Self::Gt),
868 45 => Some(Self::Ge),
869 46 => Some(Self::ElseEq),
870 47 => Some(Self::Permutation),
871 48 => Some(Self::Compare),
872 49 => Some(Self::Jump),
873 50 => Some(Self::And),
874 51 => Some(Self::Or),
875 52 => Some(Self::IsTrue),
876 53 => Some(Self::Not),
877 54 => Some(Self::BitNot),
878 55 => Some(Self::Once),
879 56 => Some(Self::If),
880 57 => Some(Self::IfNot),
881 58 => Some(Self::IsNull),
882 59 => Some(Self::IsType),
883 60 => Some(Self::ZeroOrNull),
884 61 => Some(Self::NotNull),
885 62 => Some(Self::IfNullRow),
886 63 => Some(Self::Offset),
887 64 => Some(Self::Column),
888 65 => Some(Self::TypeCheck),
889 66 => Some(Self::Affinity),
890 67 => Some(Self::MakeRecord),
891 68 => Some(Self::Count),
892 69 => Some(Self::Savepoint),
893 70 => Some(Self::AutoCommit),
894 71 => Some(Self::Transaction),
895 72 => Some(Self::ReadCookie),
896 73 => Some(Self::SetCookie),
897 74 => Some(Self::ReopenIdx),
898 75 => Some(Self::OpenRead),
899 76 => Some(Self::OpenWrite),
900 77 => Some(Self::OpenDup),
901 78 => Some(Self::OpenEphemeral),
902 79 => Some(Self::OpenAutoindex),
903 80 => Some(Self::SorterOpen),
904 81 => Some(Self::SequenceTest),
905 82 => Some(Self::OpenPseudo),
906 83 => Some(Self::Close),
907 84 => Some(Self::ColumnsUsed),
908 85 => Some(Self::SeekLT),
909 86 => Some(Self::SeekLE),
910 87 => Some(Self::SeekGE),
911 88 => Some(Self::SeekGT),
912 89 => Some(Self::SeekScan),
913 90 => Some(Self::SeekHit),
914 91 => Some(Self::IfNotOpen),
915 92 => Some(Self::IfNoHope),
916 93 => Some(Self::NoConflict),
917 94 => Some(Self::NotFound),
918 95 => Some(Self::Found),
919 96 => Some(Self::SeekRowid),
920 97 => Some(Self::NotExists),
921 98 => Some(Self::Sequence),
922 99 => Some(Self::NewRowid),
923 100 => Some(Self::Insert),
924 101 => Some(Self::RowCell),
925 102 => Some(Self::Delete),
926 103 => Some(Self::ResetCount),
927 104 => Some(Self::SorterCompare),
928 105 => Some(Self::SorterData),
929 106 => Some(Self::RowData),
930 107 => Some(Self::Rowid),
931 108 => Some(Self::NullRow),
932 109 => Some(Self::SeekEnd),
933 110 => Some(Self::Last),
934 111 => Some(Self::IfSizeBetween),
935 112 => Some(Self::SorterSort),
936 113 => Some(Self::Sort),
937 114 => Some(Self::Rewind),
938 115 => Some(Self::IfEmpty),
939 116 => Some(Self::SorterNext),
940 117 => Some(Self::Prev),
941 118 => Some(Self::Next),
942 119 => Some(Self::IdxInsert),
943 120 => Some(Self::SorterInsert),
944 121 => Some(Self::IdxDelete),
945 122 => Some(Self::DeferredSeek),
946 123 => Some(Self::IdxRowid),
947 124 => Some(Self::FinishSeek),
948 125 => Some(Self::IdxLE),
949 126 => Some(Self::IdxGT),
950 127 => Some(Self::IdxLT),
951 128 => Some(Self::IdxGE),
952 129 => Some(Self::Destroy),
953 130 => Some(Self::Clear),
954 131 => Some(Self::ResetSorter),
955 132 => Some(Self::CreateBtree),
956 133 => Some(Self::SqlExec),
957 134 => Some(Self::ParseSchema),
958 135 => Some(Self::LoadAnalysis),
959 136 => Some(Self::DropTable),
960 137 => Some(Self::DropIndex),
961 138 => Some(Self::DropTrigger),
962 139 => Some(Self::IntegrityCk),
963 140 => Some(Self::RowSetAdd),
964 141 => Some(Self::RowSetRead),
965 142 => Some(Self::RowSetTest),
966 143 => Some(Self::Program),
967 144 => Some(Self::Param),
968 145 => Some(Self::FkCounter),
969 146 => Some(Self::FkIfZero),
970 147 => Some(Self::MemMax),
971 148 => Some(Self::IfPos),
972 149 => Some(Self::OffsetLimit),
973 150 => Some(Self::IfNotZero),
974 151 => Some(Self::DecrJumpZero),
975 152 => Some(Self::AggInverse),
976 153 => Some(Self::AggStep),
977 154 => Some(Self::AggStep1),
978 155 => Some(Self::AggValue),
979 156 => Some(Self::AggFinal),
980 157 => Some(Self::Checkpoint),
981 158 => Some(Self::JournalMode),
982 159 => Some(Self::Vacuum),
983 160 => Some(Self::IncrVacuum),
984 161 => Some(Self::Expire),
985 162 => Some(Self::CursorLock),
986 163 => Some(Self::CursorUnlock),
987 164 => Some(Self::TableLock),
988 165 => Some(Self::VBegin),
989 166 => Some(Self::VCreate),
990 167 => Some(Self::VDestroy),
991 168 => Some(Self::VOpen),
992 169 => Some(Self::VCheck),
993 170 => Some(Self::VInitIn),
994 171 => Some(Self::VFilter),
995 172 => Some(Self::VColumn),
996 173 => Some(Self::VNext),
997 174 => Some(Self::VRename),
998 175 => Some(Self::VUpdate),
999 176 => Some(Self::Pagecount),
1000 177 => Some(Self::MaxPgcnt),
1001 178 => Some(Self::PureFunc),
1002 179 => Some(Self::Function),
1003 180 => Some(Self::ClrSubtype),
1004 181 => Some(Self::GetSubtype),
1005 182 => Some(Self::SetSubtype),
1006 183 => Some(Self::FilterAdd),
1007 184 => Some(Self::Filter),
1008 185 => Some(Self::Trace),
1009 186 => Some(Self::Init),
1010 187 => Some(Self::CursorHint),
1011 188 => Some(Self::Abortable),
1012 189 => Some(Self::ReleaseReg),
1013 190 => Some(Self::SetSnapshot),
1014 191 => Some(Self::Noop),
1015 192 => Some(Self::LikeConstFast),
1016 193 => Some(Self::CountIndexEqRun),
1017 194 => Some(Self::FusedAppendInsert),
1018 195 => Some(Self::FusedOpenWriteLast),
1019 196 => Some(Self::FusedLiteralResultRow),
1020 197 => Some(Self::ColumnSubstrPrefix),
1021 198 => Some(Self::ColumnOctetLength),
1022 199 => Some(Self::IfConflictSkip),
1023 _ => None,
1024 }
1025 }
1026
1027 /// Whether this opcode is a jump instruction (has a P2 jump target).
1028 pub const fn is_jump(self) -> bool {
1029 matches!(
1030 self,
1031 Self::Goto
1032 | Self::Gosub
1033 | Self::InitCoroutine
1034 | Self::Yield
1035 | Self::HaltIfNull
1036 | Self::Once
1037 | Self::If
1038 | Self::IfNot
1039 | Self::IsNull
1040 | Self::IsType
1041 | Self::NotNull
1042 | Self::IfNullRow
1043 | Self::Jump
1044 | Self::Eq
1045 | Self::Ne
1046 | Self::Lt
1047 | Self::Le
1048 | Self::Gt
1049 | Self::Ge
1050 | Self::ElseEq
1051 | Self::SeekLT
1052 | Self::SeekLE
1053 | Self::SeekGE
1054 | Self::SeekGT
1055 | Self::SeekRowid
1056 | Self::NotExists
1057 | Self::IfNotOpen
1058 | Self::IfNoHope
1059 | Self::NoConflict
1060 | Self::NotFound
1061 | Self::Found
1062 | Self::Last
1063 | Self::Rewind
1064 | Self::IfEmpty
1065 | Self::IfSizeBetween
1066 | Self::Next
1067 | Self::Prev
1068 | Self::SorterNext
1069 | Self::SorterSort
1070 | Self::Sort
1071 | Self::IdxLE
1072 | Self::IdxGT
1073 | Self::IdxLT
1074 | Self::IdxGE
1075 | Self::RowSetRead
1076 | Self::RowSetTest
1077 | Self::Program
1078 | Self::FkIfZero
1079 | Self::IfPos
1080 | Self::IfNotZero
1081 | Self::DecrJumpZero
1082 | Self::IncrVacuum
1083 | Self::VFilter
1084 | Self::VNext
1085 | Self::Filter
1086 | Self::Init
1087 | Self::IfConflictSkip
1088 )
1089 }
1090}
1091
1092impl std::fmt::Display for Opcode {
1093 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1094 f.write_str(self.name())
1095 }
1096}
1097
1098/// A single VDBE instruction.
1099#[derive(Debug, Clone, PartialEq)]
1100pub struct VdbeOp {
1101 /// The opcode.
1102 pub opcode: Opcode,
1103 /// First operand (typically a register number or cursor index).
1104 pub p1: i32,
1105 /// Second operand (often a jump target address).
1106 pub p2: i32,
1107 /// Third operand.
1108 pub p3: i32,
1109 /// Fourth operand (polymorphic: string, function pointer, collation, etc.).
1110 pub p4: P4,
1111 /// Fifth operand (small flags, typically bit flags or type mask).
1112 pub p5: u16,
1113}
1114
1115/// Metadata about an index cursor for REPLACE conflict resolution.
1116///
1117/// Used by `native_replace_row` to clean up secondary index entries when
1118/// a table row is deleted due to REPLACE conflict resolution.
1119#[derive(Debug, Clone, PartialEq, Eq)]
1120pub struct IndexCursorMeta {
1121 /// Cursor ID of the index (typically table_cursor + 1, +2, ...).
1122 pub cursor_id: i32,
1123 /// Column indices (0-based positions in the table schema) that make up
1124 /// the index key. The index key is `(col[0], col[1], ..., rowid)`.
1125 /// Empty denotes a partial or expression index whose persisted entry must
1126 /// be located by its trailing rowid during REPLACE victim cleanup.
1127 pub column_indices: Vec<usize>,
1128}
1129
1130/// The P4 operand of a VDBE instruction.
1131///
1132/// P4 is a polymorphic operand that can hold different types depending on
1133/// the opcode.
1134#[derive(Debug, Clone, PartialEq)]
1135pub enum P4 {
1136 /// No P4 value.
1137 None,
1138 /// A 32-bit integer value.
1139 Int(i32),
1140 /// A 64-bit integer value.
1141 Int64(i64),
1142 /// A 64-bit float value.
1143 Real(f64),
1144 /// A string value.
1145 Str(String),
1146 /// A blob value.
1147 Blob(Vec<u8>),
1148 /// A collation sequence name.
1149 Collation(String),
1150 /// A function name (for Function/PureFunc opcodes).
1151 FuncName(String),
1152 /// A function name with an associated collation sequence for DISTINCT
1153 /// deduplication in aggregate functions (e.g. `COUNT(DISTINCT col)` where
1154 /// `col` has `COLLATE NOCASE`).
1155 FuncNameCollated(String, String),
1156 /// A table name.
1157 Table(String),
1158 /// An index name (for IdxInsert/IdxDelete opcodes).
1159 Index(String),
1160 /// An affinity string (one char per column).
1161 Affinity(String),
1162 /// A precomputed SQLite record header template for `MakeRecord`.
1163 PrecomputedHeader(crate::record::PrecomputedRecordHeader),
1164 /// Time-travel target: commit sequence for `FOR SYSTEM_TIME AS OF COMMITSEQ <n>`.
1165 TimeTravelCommitSeq(u64),
1166 /// Time-travel target: ISO-8601 timestamp for `FOR SYSTEM_TIME AS OF '<ts>'`.
1167 TimeTravelTimestamp(String),
1168}
1169
1170// ── VDBE Program Builder ────────────────────────────────────────────────────
1171//
1172// NOTE: These types intentionally live in `fsqlite-types` so that the planner
1173// (Layer 3) can generate VDBE bytecode without depending on `fsqlite-vdbe`
1174// (Layer 5). This is enforced by the workspace layering tests (bd-1wwc).
1175
1176use fsqlite_error::{FrankenError, Result};
1177use smallvec::SmallVec;
1178
1179/// An opaque handle representing a forward-reference label.
1180///
1181/// Labels allow codegen to emit jump instructions before the target address is
1182/// known. All labels MUST be resolved before execution begins; unresolved
1183/// labels are a codegen bug.
1184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1185pub struct Label(u32);
1186
1187/// Internal tracking for label resolution.
1188#[derive(Debug)]
1189enum LabelState {
1190 /// Not yet resolved. Contains the indices of instructions whose `p2` field
1191 /// should be patched when the label is resolved.
1192 Unresolved(Vec<usize>),
1193 /// Resolved to a concrete instruction address.
1194 Resolved(i32),
1195}
1196
1197/// Sequential register allocator for the VDBE register file.
1198///
1199/// Registers are numbered starting at 1 (register 0 is reserved/unused),
1200/// matching C SQLite convention.
1201#[derive(Debug)]
1202pub struct RegisterAllocator {
1203 /// The next register number to allocate (starts at 1).
1204 next_reg: i32,
1205 /// Pool of returned temporary registers available for reuse.
1206 temp_pool: Vec<i32>,
1207}
1208
1209impl RegisterAllocator {
1210 /// Create a new allocator. First allocation returns register 1.
1211 #[must_use]
1212 pub fn new() -> Self {
1213 Self {
1214 next_reg: 1,
1215 temp_pool: Vec::new(),
1216 }
1217 }
1218
1219 /// Allocate a single persistent register.
1220 pub fn alloc_reg(&mut self) -> i32 {
1221 let reg = self.next_reg;
1222 self.next_reg += 1;
1223 reg
1224 }
1225
1226 /// Allocate a contiguous block of `n` persistent registers.
1227 ///
1228 /// Returns the first register number. The block spans `[result, result+n)`.
1229 pub fn alloc_regs(&mut self, n: i32) -> i32 {
1230 let first = self.next_reg;
1231 self.next_reg += n;
1232 first
1233 }
1234
1235 /// Allocate a temporary register (reuses from pool if available).
1236 pub fn alloc_temp(&mut self) -> i32 {
1237 self.temp_pool.pop().unwrap_or_else(|| {
1238 let reg = self.next_reg;
1239 self.next_reg += 1;
1240 reg
1241 })
1242 }
1243
1244 /// Return a temporary register to the reuse pool.
1245 pub fn free_temp(&mut self, reg: i32) {
1246 self.temp_pool.push(reg);
1247 }
1248
1249 /// The total number of registers allocated (high water mark).
1250 #[must_use]
1251 pub fn count(&self) -> i32 {
1252 self.next_reg - 1
1253 }
1254}
1255
1256impl Default for RegisterAllocator {
1257 fn default() -> Self {
1258 Self::new()
1259 }
1260}
1261
1262/// A VDBE bytecode program under construction.
1263///
1264/// Provides methods to emit instructions, create/resolve labels for forward
1265/// jumps, and allocate registers. Once construction is complete, call
1266/// [`finish`](Self::finish) to validate and extract the final instruction
1267/// sequence.
1268#[derive(Debug)]
1269pub struct ProgramBuilder {
1270 /// The instruction sequence.
1271 ops: SmallVec<[VdbeOp; 64]>,
1272 /// Label states (indexed by `Label.0`).
1273 labels: Vec<LabelState>,
1274 /// Register allocator.
1275 regs: RegisterAllocator,
1276}
1277
1278impl ProgramBuilder {
1279 /// Create a new empty program builder.
1280 #[must_use]
1281 pub fn new() -> Self {
1282 Self {
1283 ops: SmallVec::new(),
1284 labels: Vec::new(),
1285 regs: RegisterAllocator::new(),
1286 }
1287 }
1288
1289 // ── Instruction emission ────────────────────────────────────────────
1290
1291 /// Emit a single instruction and return its address (index in `ops`).
1292 pub fn emit(&mut self, op: VdbeOp) -> usize {
1293 let addr = self.ops.len();
1294 self.ops.push(op);
1295 addr
1296 }
1297
1298 /// Emit a simple instruction from parts.
1299 pub fn emit_op(&mut self, opcode: Opcode, p1: i32, p2: i32, p3: i32, p4: P4, p5: u16) -> usize {
1300 self.emit(VdbeOp {
1301 opcode,
1302 p1,
1303 p2,
1304 p3,
1305 p4,
1306 p5,
1307 })
1308 }
1309
1310 /// The current address (index of the next instruction to be emitted).
1311 #[must_use]
1312 pub fn current_addr(&self) -> usize {
1313 self.ops.len()
1314 }
1315
1316 /// Get a reference to the instruction at `addr`.
1317 #[must_use]
1318 pub fn op_at(&self, addr: usize) -> Option<&VdbeOp> {
1319 self.ops.get(addr)
1320 }
1321
1322 /// Get a mutable reference to the instruction at `addr`.
1323 #[must_use]
1324 pub fn op_at_mut(&mut self, addr: usize) -> Option<&mut VdbeOp> {
1325 self.ops.get_mut(addr)
1326 }
1327
1328 // ── Label system ────────────────────────────────────────────────────
1329
1330 /// Create a new label for forward-reference jumps.
1331 #[must_use]
1332 pub fn emit_label(&mut self) -> Label {
1333 let id = u32::try_from(self.labels.len()).expect("too many labels");
1334 self.labels.push(LabelState::Unresolved(Vec::new()));
1335 Label(id)
1336 }
1337
1338 /// Emit a jump instruction whose p2 target is a label (forward reference).
1339 ///
1340 /// The label's address will be patched into p2 when `resolve_label` is called.
1341 pub fn emit_jump_to_label(
1342 &mut self,
1343 opcode: Opcode,
1344 p1: i32,
1345 p3: i32,
1346 label: Label,
1347 p4: P4,
1348 p5: u16,
1349 ) -> usize {
1350 let addr = self.emit(VdbeOp {
1351 opcode,
1352 p1,
1353 p2: -1, // placeholder; will be patched
1354 p3,
1355 p4,
1356 p5,
1357 });
1358
1359 let state = self
1360 .labels
1361 .get_mut(usize::try_from(label.0).expect("label fits usize"))
1362 .expect("label must exist");
1363
1364 match state {
1365 LabelState::Unresolved(refs) => refs.push(addr),
1366 LabelState::Resolved(target) => {
1367 // Label already resolved; patch immediately.
1368 self.ops[addr].p2 = *target;
1369 }
1370 }
1371
1372 addr
1373 }
1374
1375 /// Resolve a label to the current address and patch all forward refs.
1376 pub fn resolve_label(&mut self, label: Label) {
1377 let addr = i32::try_from(self.current_addr()).expect("program too large");
1378 self.resolve_label_to(label, addr);
1379 }
1380
1381 /// Resolve a label to an explicit address (used for some control patterns).
1382 pub fn resolve_label_to(&mut self, label: Label, address: i32) {
1383 let idx = usize::try_from(label.0).expect("label fits usize");
1384 let state = self.labels.get_mut(idx).expect("label must exist");
1385
1386 match state {
1387 LabelState::Unresolved(refs) => {
1388 // Patch all references.
1389 for &ref_addr in refs.iter() {
1390 self.ops[ref_addr].p2 = address;
1391 }
1392 *state = LabelState::Resolved(address);
1393 }
1394 LabelState::Resolved(_) => {
1395 // Idempotent: resolving twice is allowed as long as it's consistent.
1396 *state = LabelState::Resolved(address);
1397 }
1398 }
1399 }
1400
1401 // ── Register allocation ─────────────────────────────────────────────
1402
1403 /// Allocate a single persistent register.
1404 pub fn alloc_reg(&mut self) -> i32 {
1405 self.regs.alloc_reg()
1406 }
1407
1408 /// Allocate a contiguous block of persistent registers.
1409 pub fn alloc_regs(&mut self, n: i32) -> i32 {
1410 self.regs.alloc_regs(n)
1411 }
1412
1413 /// Allocate a temporary register (reusable).
1414 pub fn alloc_temp(&mut self) -> i32 {
1415 self.regs.alloc_temp()
1416 }
1417
1418 /// Return a temporary register to the pool.
1419 pub fn free_temp(&mut self, reg: i32) {
1420 self.regs.free_temp(reg);
1421 }
1422
1423 /// Total registers allocated (high water mark).
1424 #[must_use]
1425 pub fn register_count(&self) -> i32 {
1426 self.regs.count()
1427 }
1428
1429 // ── Peephole Passes (IMPL-13) ───────────────────────────────────────
1430
1431 /// Fuse `Integer(lit, reg) + ResultRow(reg, 1)` pairs into
1432 /// `FusedLiteralResultRow(lit, reg)` + `Noop`.
1433 ///
1434 /// Rewrites in-place so program counters, jump targets, and the label
1435 /// tables remain valid without rewiring. The `ResultRow` is replaced by a
1436 /// `Noop` rather than removed so no following instruction shifts.
1437 ///
1438 /// Conservative preconditions per fusion site:
1439 /// - The `Integer`'s target register equals the `ResultRow`'s start
1440 /// register.
1441 /// - The `ResultRow` emits exactly one column (`p2 == 1`).
1442 /// - The `ResultRow` is NOT a resolved jump target from any prior jump
1443 /// in this program (a mid-pair jump would otherwise skip the Integer
1444 /// write and run `ResultRow` against an unrelated register value).
1445 /// - Neither instruction carries a non-`None` P4 payload (Integer/ResultRow
1446 /// don't use P4 in their canonical form).
1447 /// - Both instructions carry P5 == 0 and P3 == 0.
1448 ///
1449 /// Returns the number of fusions performed.
1450 pub fn apply_fuse_literal_result_row(&mut self) -> usize {
1451 // Collect the set of resolved jump targets. Any address that is the
1452 // target of some jump instruction's `p2` is ineligible to be the
1453 // second half of a fusion pair.
1454 let mut jump_targets: std::collections::HashSet<i32> = std::collections::HashSet::new();
1455 for op in &self.ops {
1456 if op.opcode.is_jump() {
1457 jump_targets.insert(op.p2);
1458 }
1459 }
1460
1461 let mut fused = 0usize;
1462 let len = self.ops.len();
1463 let mut i = 0;
1464 while i + 1 < len {
1465 let is_int = matches!(self.ops[i].opcode, Opcode::Integer)
1466 && self.ops[i].p3 == 0
1467 && self.ops[i].p5 == 0
1468 && matches!(self.ops[i].p4, P4::None);
1469 let is_row = matches!(self.ops[i + 1].opcode, Opcode::ResultRow)
1470 && self.ops[i + 1].p2 == 1
1471 && self.ops[i + 1].p3 == 0
1472 && self.ops[i + 1].p5 == 0
1473 && matches!(self.ops[i + 1].p4, P4::None);
1474 let same_reg = is_int && is_row && self.ops[i].p2 == self.ops[i + 1].p1;
1475 let row_addr = i32::try_from(i + 1).ok();
1476 let row_is_target = row_addr.is_some_and(|a| jump_targets.contains(&a));
1477
1478 if same_reg && !row_is_target {
1479 let lit = self.ops[i].p1;
1480 let reg = self.ops[i].p2;
1481 self.ops[i] = VdbeOp {
1482 opcode: Opcode::FusedLiteralResultRow,
1483 p1: lit,
1484 p2: reg,
1485 p3: 0,
1486 p4: P4::None,
1487 p5: 0,
1488 };
1489 self.ops[i + 1] = VdbeOp {
1490 opcode: Opcode::Noop,
1491 p1: 0,
1492 p2: 0,
1493 p3: 0,
1494 p4: P4::None,
1495 p5: 0,
1496 };
1497 fused += 1;
1498 i += 2;
1499 } else {
1500 i += 1;
1501 }
1502 }
1503 fused
1504 }
1505
1506 // ── Finalization ────────────────────────────────────────────────────
1507
1508 /// Validate all labels are resolved and return the finished program.
1509 pub fn finish(self) -> Result<VdbeProgram> {
1510 // Check for unresolved labels.
1511 for (i, state) in self.labels.iter().enumerate() {
1512 if let LabelState::Unresolved(refs) = state
1513 && !refs.is_empty()
1514 {
1515 return Err(FrankenError::Internal(format!(
1516 "unresolved label {i} referenced by {} instruction(s)",
1517 refs.len()
1518 )));
1519 }
1520 }
1521
1522 Ok(VdbeProgram {
1523 ops: self.ops,
1524 register_count: self.regs.count(),
1525 })
1526 }
1527}
1528
1529impl Default for ProgramBuilder {
1530 fn default() -> Self {
1531 Self::new()
1532 }
1533}
1534
1535/// A finalized VDBE bytecode program ready for execution.
1536#[derive(Debug, Clone, PartialEq)]
1537pub struct VdbeProgram {
1538 /// The instruction sequence.
1539 ops: SmallVec<[VdbeOp; 64]>,
1540 /// Number of registers needed (high water mark from allocation).
1541 register_count: i32,
1542}
1543
1544impl VdbeProgram {
1545 /// The instruction sequence.
1546 #[must_use]
1547 pub fn ops(&self) -> &[VdbeOp] {
1548 &self.ops
1549 }
1550
1551 /// Number of instructions.
1552 #[must_use]
1553 pub fn len(&self) -> usize {
1554 self.ops.len()
1555 }
1556
1557 /// Whether the program is empty.
1558 #[must_use]
1559 pub fn is_empty(&self) -> bool {
1560 self.ops.is_empty()
1561 }
1562
1563 /// Number of registers required.
1564 #[must_use]
1565 pub fn register_count(&self) -> i32 {
1566 self.register_count
1567 }
1568
1569 /// Get the instruction at the given program counter.
1570 #[must_use]
1571 pub fn get(&self, pc: usize) -> Option<&VdbeOp> {
1572 self.ops.get(pc)
1573 }
1574
1575 /// Disassemble the program to a human-readable string.
1576 ///
1577 /// Output format matches SQLite's `EXPLAIN` output.
1578 #[must_use]
1579 pub fn disassemble(&self) -> String {
1580 use std::fmt::Write;
1581
1582 let mut out = std::string::String::with_capacity(self.ops.len() * 60);
1583 out.push_str("addr opcode p1 p2 p3 p4 p5\n");
1584 out.push_str("---- --------------- ---- ---- ---- ----------------- --\n");
1585
1586 for (addr, op) in self.ops.iter().enumerate() {
1587 let p4_str = match &op.p4 {
1588 P4::None => String::new(),
1589 P4::Int(v) => format!("(int){v}"),
1590 P4::Int64(v) => format!("(i64){v}"),
1591 P4::Real(v) => format!("(real){v}"),
1592 P4::Str(s) => format!("(str){s}"),
1593 P4::Blob(b) => format!("(blob)[{}B]", b.len()),
1594 P4::Collation(c) => format!("(coll){c}"),
1595 P4::FuncName(f) => format!("(func){f}"),
1596 P4::FuncNameCollated(f, c) => format!("(func){f} coll={c}"),
1597 P4::Table(t) => format!("(tbl){t}"),
1598 P4::Index(i) => format!("(idx){i}"),
1599 P4::Affinity(a) => format!("(aff){a}"),
1600 P4::PrecomputedHeader(header) => format!("(hdr)[{}B]", header.template.len()),
1601 P4::TimeTravelCommitSeq(seq) => format!("(tt-seq){seq}"),
1602 P4::TimeTravelTimestamp(ts) => format!("(tt-ts){ts}"),
1603 };
1604
1605 writeln!(
1606 &mut out,
1607 "{addr:<4} {:<15} {:<4} {:<4} {:<4} {:<17} {:<2}",
1608 op.opcode.name(),
1609 op.p1,
1610 op.p2,
1611 op.p3,
1612 p4_str,
1613 op.p5,
1614 )
1615 .expect("write to string");
1616 }
1617
1618 out
1619 }
1620}
1621
1622#[cfg(test)]
1623#[allow(clippy::approx_constant)]
1624mod tests {
1625 use super::*;
1626 use std::collections::HashSet;
1627
1628 #[test]
1629 fn opcode_count() {
1630 // COUNT is the exclusive upper bound on discriminants (1..COUNT), so the
1631 // number of opcodes actually defined is COUNT - 1.
1632 assert_eq!(Opcode::COUNT, 200);
1633 assert_eq!(Opcode::COUNT - 1, 199);
1634 }
1635
1636 #[test]
1637 fn opcode_name_roundtrip() {
1638 // Spot check a few opcodes
1639 assert_eq!(Opcode::Goto.name(), "Goto");
1640 assert_eq!(Opcode::Halt.name(), "Halt");
1641 assert_eq!(Opcode::Insert.name(), "Insert");
1642 assert_eq!(Opcode::Delete.name(), "Delete");
1643 assert_eq!(Opcode::ResultRow.name(), "ResultRow");
1644 assert_eq!(Opcode::Noop.name(), "Noop");
1645 }
1646
1647 #[test]
1648 fn opcode_from_byte() {
1649 assert_eq!(Opcode::from_byte(0), None);
1650 assert_eq!(Opcode::from_byte(1), Some(Opcode::Goto));
1651 assert_eq!(Opcode::from_byte(8), Some(Opcode::Halt));
1652 assert_eq!(Opcode::from_byte(190), Some(Opcode::SetSnapshot));
1653 assert_eq!(Opcode::from_byte(191), Some(Opcode::Noop));
1654 assert_eq!(Opcode::from_byte(192), Some(Opcode::LikeConstFast));
1655 assert_eq!(Opcode::from_byte(196), Some(Opcode::FusedLiteralResultRow));
1656 assert_eq!(Opcode::from_byte(197), Some(Opcode::ColumnSubstrPrefix));
1657 assert_eq!(Opcode::from_byte(198), Some(Opcode::ColumnOctetLength));
1658 assert_eq!(Opcode::from_byte(199), Some(Opcode::IfConflictSkip));
1659 assert_eq!(Opcode::from_byte(200), None);
1660 assert_eq!(Opcode::from_byte(255), None);
1661 }
1662
1663 #[test]
1664 fn opcode_from_byte_exhaustive() {
1665 // Every assigned opcode byte should produce Some.
1666 for i in 1..Opcode::COUNT as u8 {
1667 assert!(
1668 Opcode::from_byte(i).is_some(),
1669 "from_byte({i}) returned None"
1670 );
1671 }
1672 }
1673
1674 #[test]
1675 fn test_opcode_distinct_u8_values() {
1676 let mut encoded = HashSet::new();
1677 for byte in 1..Opcode::COUNT as u8 {
1678 let opcode = Opcode::from_byte(byte).expect("opcode byte must decode");
1679 let inserted = encoded.insert(opcode as u8);
1680 assert!(inserted, "duplicate opcode byte value for {:?}", opcode);
1681 }
1682
1683 assert_eq!(
1684 encoded.len(),
1685 Opcode::COUNT - 1,
1686 "every opcode must map to a unique byte"
1687 );
1688 }
1689
1690 #[test]
1691 fn opcode_display() {
1692 assert_eq!(Opcode::Goto.to_string(), "Goto");
1693 assert_eq!(Opcode::Init.to_string(), "Init");
1694 }
1695
1696 #[test]
1697 fn opcode_is_jump() {
1698 assert!(Opcode::Goto.is_jump());
1699 assert!(Opcode::If.is_jump());
1700 assert!(Opcode::IfNot.is_jump());
1701 assert!(Opcode::Eq.is_jump());
1702 assert!(Opcode::Next.is_jump());
1703 assert!(Opcode::Rewind.is_jump());
1704 assert!(Opcode::Init.is_jump());
1705
1706 assert!(!Opcode::Integer.is_jump());
1707 assert!(!Opcode::Add.is_jump());
1708 assert!(!Opcode::Insert.is_jump());
1709 assert!(!Opcode::Noop.is_jump());
1710 assert!(!Opcode::ResultRow.is_jump());
1711 }
1712
1713 #[test]
1714 fn vdbe_op_basic() {
1715 let op = VdbeOp {
1716 opcode: Opcode::Integer,
1717 p1: 42,
1718 p2: 1,
1719 p3: 0,
1720 p4: P4::None,
1721 p5: 0,
1722 };
1723 assert_eq!(op.opcode, Opcode::Integer);
1724 assert_eq!(op.p1, 42);
1725 }
1726
1727 #[test]
1728 fn p4_variants() {
1729 let p4 = P4::Int(42);
1730 assert_eq!(p4, P4::Int(42));
1731
1732 let p4 = P4::Str("hello".to_owned());
1733 assert_eq!(p4, P4::Str("hello".to_owned()));
1734
1735 let p4 = P4::Real(3.14);
1736 assert_eq!(p4, P4::Real(3.14));
1737 }
1738}