// Copyright 2025 Dolthub, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package plpgsql import ( "fmt" "strconv" "strings" "github.com/cockroachdb/errors" ) // action exists to match the expected JSON format. type action struct { StmtBlock plpgSQL_stmt_block `json:"PLpgSQL_stmt_block"` } // cond exists to match the expected JSON format. type cond struct { Expression plpgSQL_expr `json:"PLpgSQL_expr"` } // datatype exists to match the expected JSON format. type datatype struct { Type plpgSQL_type `json:"PLpgSQL_type"` } // default_val exists to match the expected JSON format. type default_val struct { Var plpgSQL_expr `json:"PLpgSQL_expr"` } // datum exists to match the expected JSON format. type datum struct { Record *plpgSQL_rec `json:"PLpgSQL_rec"` RecordField *plpgSQL_recfield `json:"PLpgSQL_recfield"` Row *plpgSQL_row `json:"PLpgSQL_row"` Variable *plpgSQL_var `json:"PLpgSQL_var"` } // elsif exists to match the expected JSON format. type elsif struct { ElseIf plpgSQL_if_elsif `json:"PLpgSQL_if_elsif"` } // expr exists to match the expected JSON format. type expr struct { Expression plpgSQL_expr `json:"PLpgSQL_expr"` } // field exists to match the expected JSON format. type field struct { Name string `json:"name"` VariableNumber int32 `json:"varno"` } // function exists to match the expected JSON format. type function struct { Function plpgSQL_block `json:"PLpgSQL_function"` } // plpgSQL_block exists to match the expected JSON format. type plpgSQL_block struct { NewVariableNumber int32 `json:"new_varno"` OldVariableNumber int32 `json:"old_varno"` Datums []datum `json:"datums"` Action action `json:"action"` } // plpgSQL_expr exists to match the expected JSON format. type plpgSQL_expr struct { Query string `json:"query"` ParseMode int32 `json:"parseMode"` } // plpgSQL_if_elsif exists to match the expected JSON format. type plpgSQL_if_elsif struct { Condition cond `json:"cond"` Then []statement `json:"stmts"` LineNumber int32 `json:"lineno"` } // plpgSQL_rec exists to match the expected JSON format. type plpgSQL_rec struct { RefName string `json:"refname"` DatumNumber int32 `json:"dno"` } // plpgSQL_recfield exists to match the expected JSON format. type plpgSQL_recfield struct { FieldName string `json:"fieldname"` RecordParentNumber int32 `json:"recparentno"` } // plpgSQL_row exists to match the expected JSON format. type plpgSQL_row struct { RefName string `json:"refname"` Fields []field `json:"fields"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_assign exists to match the expected JSON format. type plpgSQL_stmt_assign struct { Expression expr `json:"expr"` VariableNumber int32 `json:"varno"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_block exists to match the expected JSON format. type plpgSQL_stmt_block struct { Body []statement `json:"body"` Label string `json:"label"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_call exists to match the expected JSON format. type plpgSQL_stmt_call struct { LineNumber int32 `json:"lineno"` Expression expr `json:"expr"` IsCall bool `json:"is_call"` Target datum `json:"target"` } // plpgSQL_stmt_case exists to match the expected JSON format. type plpgSQL_stmt_case struct { LineNumber int32 `json:"lineno"` Expression expr `json:"t_expr"` // VarNo indicates the ID for the __Case__Variable_N__ variable that holds the evaluated // value of the case expression. VarNo int32 `json:"t_varno"` WhenList []statement `json:"case_when_list"` HasElse bool `json:"have_else"` Else []statement `json:"else_stmts"` } // plpgSQL_stmt_dynexecute exists to match the expected JSON format. type plpgSQL_stmt_dynexecute struct { LineNumber int32 `json:"lineno"` Into bool `json:"into"` Query expr `json:"query"` Target datum `json:"target"` Params []sqlstmt `json:"params"` } // plpgSQL_case_when exists to match the expected JSON format. type plpgSQL_case_when struct { LineNumber int32 `json:"lineno"` Expression expr `json:"expr"` Body []statement `json:"stmts"` } // plpgSQL_stmt_execsql exists to match the expected JSON format. type plpgSQL_stmt_execsql struct { SQLStmt sqlstmt `json:"sqlstmt"` LineNumber int32 `json:"lineno"` Into bool `json:"into"` Target datum `json:"target"` } // plpgSQL_stmt_exit exists to match the expected JSON format. type plpgSQL_stmt_exit struct { Label string `json:"label"` IsExit bool `json:"is_exit"` Condition *expr `json:"cond"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_fori exists to match the expected JSON format. type plpgSQL_stmt_fori struct { Label string `json:"label"` Var datum `json:"var"` Lower *expr `json:"lower"` Upper *expr `json:"upper"` Step *expr `json:"step"` Reverse bool `json:"reverse"` Body []statement `json:"body"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_fors exists to match the expected JSON format. type plpgSQL_stmt_fors struct { Label string `json:"label"` Var datum `json:"var"` Body []statement `json:"body"` Query *expr `json:"query"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_if exists to match the expected JSON format. type plpgSQL_stmt_if struct { Condition cond `json:"cond"` Then []statement `json:"then_body"` ElseIf []elsif `json:"elsif_list"` Else []statement `json:"else_body"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_loop exists to match the expected JSON format. type plpgSQL_stmt_loop struct { Body []statement `json:"body"` Label string `json:"label"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_perform exists to match the expected JSON format. type plpgSQL_stmt_perform struct { Expression expr `json:"expr"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_raise exists to match the expected JSON format. type plpgSQL_stmt_raise struct { LineNumber int32 `json:"lineno"` ELogLevel int32 `json:"elog_level"` Message string `json:"message"` Params []sqlstmt `json:"params"` Options []plpgSQL_raise_option_wrapper `json:"options"` } // plpgSQL_raise_option_wrapper exists to match the expected JSON format. type plpgSQL_raise_option_wrapper struct { Option plpgSQL_raise_option `json:"PLpgSQL_raise_option"` } // plpgSQL_raise_option exists to match the expected JSON format. type plpgSQL_raise_option struct { OptionType int32 `json:"opt_type"` Expression sqlstmt `json:"expr"` } // plpgSQL_stmt_return exists to match the expected JSON format. type plpgSQL_stmt_return struct { Expression expr `json:"expr"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_return_query exists to match the expected JSON format. type plpgSQL_stmt_return_query struct { Query expr `json:"query"` LineNumber int32 `json:"lineno"` } // plpgSQL_stmt_while exists to match the expected JSON format. type plpgSQL_stmt_while struct { Condition cond `json:"cond"` Body []statement `json:"body"` Label string `json:"label"` LineNumber int32 `json:"lineno"` } // plpgSQL_type exists to match the expected JSON format. type plpgSQL_type struct { Name string `json:"typname"` } // plpgSQL_var exists to match the expected JSON format. type plpgSQL_var struct { RefName string `json:"refname"` Type datatype `json:"datatype"` LineNumber int32 `json:"lineno"` Default default_val `json:"default_val"` } // sqlstmt exists to match the expected JSON format. type sqlstmt struct { Expr plpgSQL_expr `json:"PLpgSQL_expr"` } // statement exists to match the expected JSON format. Unlike other structs, this is used like a union rather than // having a singular expected implementation. type statement struct { Assignment *plpgSQL_stmt_assign `json:"PLpgSQL_stmt_assign"` Block *plpgSQL_stmt_block `json:"PLpgSQL_stmt_block"` Call *plpgSQL_stmt_call `json:"PLpgSQL_stmt_call"` Case *plpgSQL_stmt_case `json:"PLpgSQL_stmt_case"` DynExec *plpgSQL_stmt_dynexecute `json:"PLpgSQL_stmt_dynexecute"` ExecSQL *plpgSQL_stmt_execsql `json:"PLpgSQL_stmt_execsql"` Exit *plpgSQL_stmt_exit `json:"PLpgSQL_stmt_exit"` ForILoop *plpgSQL_stmt_fori `json:"PLpgSQL_stmt_fori"` ForSLoop *plpgSQL_stmt_fors `json:"PLpgSQL_stmt_fors"` If *plpgSQL_stmt_if `json:"PLpgSQL_stmt_if"` Loop *plpgSQL_stmt_loop `json:"PLpgSQL_stmt_loop"` Perform *plpgSQL_stmt_perform `json:"PLpgSQL_stmt_perform"` Raise *plpgSQL_stmt_raise `json:"PLpgSQL_stmt_raise"` Return *plpgSQL_stmt_return `json:"PLpgSQL_stmt_return"` ReturnQuery *plpgSQL_stmt_return_query `json:"PLpgSQL_stmt_return_query"` When *plpgSQL_case_when `json:"PLpgSQL_case_when"` While *plpgSQL_stmt_while `json:"PLpgSQL_stmt_while"` } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_assign) Convert() (Assignment, error) { query := stmt.Expression.Expression.Query varName := "" if equalsIdx := strings.Index(query, ":="); equalsIdx > 0 { varName = strings.TrimSpace(query[:equalsIdx]) query = strings.TrimSpace(query[equalsIdx+2:]) } else if equalsIdx = strings.Index(query, "="); equalsIdx > 0 { varName = strings.TrimSpace(query[:equalsIdx]) query = strings.TrimSpace(query[equalsIdx+1:]) } else { return Assignment{}, errors.New("PL/pgSQL assignment cannot find `:=` sign") } return Assignment{ VariableName: varName, Expression: query, VariableIndex: stmt.VariableNumber, }, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_call) Convert() (ExecuteSQL, error) { var target string if !stmt.IsCall { if stmt.Target.Row != nil { names := make([]string, len(stmt.Target.Row.Fields)) for i, rowField := range stmt.Target.Row.Fields { names[i] = rowField.Name } target = strings.Join(names, ",") } else if stmt.Target.Variable != nil { target = stmt.Target.Variable.RefName } else { return ExecuteSQL{}, errors.Errorf("unhandled datum type: %T", stmt.Target) } } return ExecuteSQL{ Statement: stmt.Expression.Expression.Query, Target: target, }, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_case) Convert() (block Block, err error) { // If the CASE statement has a main expression, start by assigning it to a variable so // we can evaluate it once and only once. if stmt.Expression.Expression.Query != "" { // TODO: pg_query_go creates the definitions for these variables, and // ideally users shouldn't be able to reference them. We could // update all the references to them (i.e. declaration, assignment, // and WHEN block exprs) to change the name to include a \0 char to // prevent users from referencing them or colliding with them. block.Body = append(block.Body, Assignment{ VariableName: fmt.Sprintf("__Case__Variable_%d__", stmt.VarNo), Expression: stmt.Expression.Expression.Query, }) } // Record indexes of all the GOTO ops that jump to the very end of the case block so we // can update them later and plug in the correct offsets after we know the final size. var gotoEndOpsIndexes []int // Add operations for each WHEN statement... for _, stmt := range stmt.WhenList { when := stmt.When if when == nil { return Block{}, fmt.Errorf("case statement WHEN clause is nil") } // TODO: The generated expressions from pg_query_go uses double quotes // around the variable name, which is valid for Postgres, but // our engine doesn't currently resolve double-quoted strings to // variables, so for now, we just extract the double quotes. expressionString := when.Expression.Expression.Query expressionString = strings.ReplaceAll(expressionString, `"`, "") convertedWhenBodyStatements, err := jsonConvertStatements(when.Body) if err != nil { return Block{}, err } block.Body = append(block.Body, If{ Condition: expressionString, GotoOffset: 2, }, Goto{ // This GOTO jumps to the next WHEN block, so step over all the statements // from this WHEN block, plus 1 for the GOTO op we add at the end of each // block, and plus 1 more to move to the next statement. Offset: int32(len(convertedWhenBodyStatements) + 1 + 1), }) block.Body = append(block.Body, convertedWhenBodyStatements...) // Add a GOTO op to jump to the end of the entire CASE block, and record its position // in the statement block so we can update it later. block.Body = append(block.Body, Goto{}) gotoEndOpsIndexes = append(gotoEndOpsIndexes, len(block.Body)-1) } if stmt.HasElse { convertElseBodyStatements, err := jsonConvertStatements(stmt.Else) if err != nil { return Block{}, err } block.Body = append(block.Body, convertElseBodyStatements...) // TODO: If no cases match and there is no ELSE block, then add a RAISE statement // to return an error. //} else { // Sample PostgreSQL error response: // ERROR: case not found // HINT: CASE statement is missing ELSE part. // CONTEXT: PL/pgSQL function interpreted_case(integer) line 5 at CASE } // Update all the GOTO ops that jump to the very end of the case block. for _, gotoEndOpIndex := range gotoEndOpsIndexes { // Sanity check that we are looking at a GOTO statement if _, ok := block.Body[gotoEndOpIndex].(Goto); !ok { return Block{}, fmt.Errorf("expected Goto statement, got %T", block.Body[gotoEndOpIndex]) } block.Body[gotoEndOpIndex] = Goto{ Offset: int32(len(block.Body) - gotoEndOpIndex), } } return block, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_dynexecute) Convert() (DynamicExecute, error) { var params []string for _, param := range stmt.Params { params = append(params, param.Expr.Query) } var target string if stmt.Into { switch { case stmt.Target.Row != nil: names := make([]string, len(stmt.Target.Row.Fields)) for i, rowField := range stmt.Target.Row.Fields { names[i] = rowField.Name } target = strings.Join(names, ",") case stmt.Target.Variable != nil: target = stmt.Target.Variable.RefName default: return DynamicExecute{}, errors.Errorf("unhandled datum type: %T", stmt.Target) } } query := strings.TrimSuffix(strings.TrimPrefix(stmt.Query.Expression.Query, "'"), "'") return DynamicExecute{ Query: query, Params: params, Target: target, }, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_execsql) Convert() (ExecuteSQL, error) { var target string if stmt.Into { if stmt.Target.Row != nil { names := make([]string, len(stmt.Target.Row.Fields)) for i, rowField := range stmt.Target.Row.Fields { names[i] = rowField.Name } target = strings.Join(names, ",") } else if stmt.Target.Variable != nil { target = stmt.Target.Variable.RefName } else { return ExecuteSQL{}, errors.Errorf("unhandled datum type: %T", stmt.Target) } } return ExecuteSQL{ Statement: stmt.SQLStmt.Expr.Query, Target: target, }, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_exit) Convert() Statement { offset := int32(-1) if stmt.IsExit { offset = 1 } var gotoStmt Goto if len(stmt.Label) > 0 { gotoStmt = Goto{ Offset: offset, Label: stmt.Label, } } else { gotoStmt = Goto{ Offset: offset, NearestScopeOp: true, } } if stmt.Condition == nil { return gotoStmt } else { return Block{ Body: []Statement{ If{ Condition: stmt.Condition.Expression.Query, GotoOffset: 2, }, Goto{Offset: 2}, gotoStmt, }, } } } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_fori) Convert() (block Block, err error) { block.Label = stmt.Label block.IsLoop = true if stmt.Var.Variable == nil { return Block{}, errors.New("for loop variable cannot be nil") } varName := stmt.Var.Variable.RefName // Extract bound and step expressions lowerExpr := "1" if stmt.Lower != nil { lowerExpr = stmt.Lower.Expression.Query } upperExpr := "1" if stmt.Upper != nil { upperExpr = stmt.Upper.Expression.Query } stepExpr := "1" if stmt.Step != nil { stepExpr = stmt.Step.Expression.Query } // Determine init value, loop condition, and increment expression based on direction. // In the JSON, Lower is always the starting value and Upper is the ending bound. var condition, incrExpr string if stmt.Reverse { condition = fmt.Sprintf("%s >= (%s)", varName, upperExpr) incrExpr = fmt.Sprintf("%s - (%s)", varName, stepExpr) } else { condition = fmt.Sprintf("%s <= (%s)", varName, upperExpr) incrExpr = fmt.Sprintf("%s + (%s)", varName, stepExpr) } // Convert the loop body. convertedBody, err := jsonConvertStatements(stmt.Body) if err != nil { return Block{}, err } bodySize := OperationSizeForStatements(convertedBody) // Build the loop body: // [0] InitAssign: varName := lower // [1] If(condition, GotoOffset:2) → jumps to [3] (first body stmt) when true // [2] ExitGoto → offset=3+bodySize → jumps to ScopeEnd // [3..3+N-1] body statements (N = bodySize) // [3+N] IncrAssign: varName := varName +/- step // [3+N+1] BackGoto → offset=-(3+bodySize) → jumps back to If at [1] // // Because no variables are declared in this block (the loop variable is already // declared by the caller's DECLARE section), ScopeBegin is at M and the // InitAssign is at M+1, so all offsets are consistent. block.Body = []Statement{ Assignment{ VariableName: varName, Expression: lowerExpr, }, If{ Condition: condition, GotoOffset: 2, }, Goto{ Offset: 3 + bodySize, }, } block.Body = append(block.Body, convertedBody...) block.Body = append(block.Body, Assignment{ VariableName: varName, Expression: incrExpr, }, Goto{ Offset: -(3 + bodySize), }, ) return block, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_fors) Convert() (block Block, err error) { block.Label = stmt.Label block.IsLoop = true if stmt.Query == nil { return Block{}, errors.New("FOR..IN..SELECT loop must have a query") } var varName string switch { case stmt.Var.Record != nil: varName = stmt.Var.Record.RefName case stmt.Var.Variable != nil: varName = stmt.Var.Variable.RefName case stmt.Var.Row != nil: varName = stmt.Var.Row.RefName default: return Block{}, errors.New("FOR..IN..SELECT loop variable must be a record, row, or variable") } // Use the line number to keep cursor names unique across multiple ForS loops // that might use the same variable name. cursorName := fmt.Sprintf("__cursor_%s_%d__", varName, stmt.LineNumber) query := stmt.Query.Expression.Query convertedBody, err := jsonConvertStatements(stmt.Body) if err != nil { return Block{}, err } bodySize := OperationSizeForStatements(convertedBody) // Layout inside the block (ScopeBegin/ScopeEnd are added by Block.AppendOperations): // [0] ForQueryInit – execute query, store rows in cursor // [1] ForQueryNext – fetch next row into varName, or jump forward by (bodySize+2) to ScopeEnd // [2..2+bodySize-1] body statements // [2+bodySize] Goto back to ForQueryNext: offset = -(1 + bodySize) block.Body = []Statement{ ForQueryInit{CursorName: cursorName, Query: query}, ForQueryNext{CursorName: cursorName, RecordVar: varName, GotoOffset: bodySize + 2}, } block.Body = append(block.Body, convertedBody...) block.Body = append(block.Body, Goto{Offset: -(1 + bodySize)}) return block, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_if) Convert() (Block, error) { // We store all GOTOs that will need to go to the end of the block. Since we can't know that ahead of time, we store // their indexes and set them at the end of the function. type gotoEndIndex struct { BodyIndex int GotoIndex int32 } var gotoEndIndexes []gotoEndIndex returnBlock := Block{ Body: []Statement{ If{ Condition: stmt.Condition.Expression.Query, GotoOffset: 2, // The operation following the conditional skips the THEN statements, so we're skipping that }, }, } // We'll parse our THEN statements, but we won't add them to the block just yet as we need their operation sizes thenStmts, err := jsonConvertStatements(stmt.Then) if err != nil { return Block{}, err } // When the condition is false, we want to skip our THEN block, so we do that (plus the GOTO which finishes the THEN block) returnBlock.Body = append(returnBlock.Body, Goto{Offset: OperationSizeForStatements(thenStmts) + 2}) // Then we'll append our THEN block returnBlock.Body = append(returnBlock.Body, thenStmts...) // Then we want to append the GOTO that finishes the THEN block, but we don't know the end just yet, so we'll save // its index and fill it in later gotoEndIndexes = append(gotoEndIndexes, gotoEndIndex{ BodyIndex: len(returnBlock.Body), GotoIndex: OperationSizeForStatements(returnBlock.Body), }) returnBlock.Body = append(returnBlock.Body, Goto{}) // We repeat the same process for each ELSIF statement (refer to the comments above) for _, elseIf := range stmt.ElseIf { returnBlock.Body = append(returnBlock.Body, If{ Condition: elseIf.ElseIf.Condition.Expression.Query, GotoOffset: 2, // Same rules as skipping our THEN statement above }) elseIfStmts, err := jsonConvertStatements(elseIf.ElseIf.Then) if err != nil { return Block{}, err } returnBlock.Body = append(returnBlock.Body, Goto{Offset: OperationSizeForStatements(elseIfStmts) + 2}) returnBlock.Body = append(returnBlock.Body, elseIfStmts...) gotoEndIndexes = append(gotoEndIndexes, gotoEndIndex{ BodyIndex: len(returnBlock.Body), GotoIndex: OperationSizeForStatements(returnBlock.Body), }) returnBlock.Body = append(returnBlock.Body, Goto{}) } // Finally we handle our ELSE statements. We don't have a condition to check, so we don't have to append any // additional GOTOs. elseStmts, err := jsonConvertStatements(stmt.Else) if err != nil { return Block{}, err } returnBlock.Body = append(returnBlock.Body, elseStmts...) // Now we'll set all of our GOTOs so that they skip to the end of the block. // We have to take their index position into account, since we want to skip to the end from their relative position. for _, idx := range gotoEndIndexes { returnBlock.Body[idx.BodyIndex] = Goto{Offset: OperationSizeForStatements(returnBlock.Body) - idx.GotoIndex} } return returnBlock, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_loop) Convert() (block Block, err error) { // Set the block's label if one was provided block.Label = stmt.Label block.IsLoop = true // Convert the body of the loop first so we can determine the GOTO offset block.Body, err = jsonConvertStatements(stmt.Body) if err != nil { return Block{}, err } // The loop returns to the beginning of the loop, skipping the body block.Body = append(block.Body, Goto{Offset: -OperationSizeForStatements(block.Body)}) return block, nil } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_perform) Convert() Perform { return Perform{ Statement: stmt.Expression.Expression.Query, } } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_raise) Convert() Raise { var params []string for _, param := range stmt.Params { params = append(params, param.Expr.Query) } options := make(map[string]string) for _, option := range stmt.Options { options[strconv.Itoa(int(option.Option.OptionType))] = option.Option.Expression.Expr.Query } return Raise{ Level: NoticeLevel(uint8(stmt.ELogLevel)).String(), Message: stmt.Message, Params: params, Options: options, } } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_return) Convert() Return { return Return{ Expression: stmt.Expression.Expression.Query, } } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_return_query) Convert() ReturnQuery { return ReturnQuery{ Query: stmt.Query.Expression.Query, } } // Convert converts the JSON statement into its output form. func (stmt *plpgSQL_stmt_while) Convert() (block Block, err error) { // Convert the body of the loop first so we can determine the GOTO offsets convertedLoopBodyStmts, err := jsonConvertStatements(stmt.Body) if err != nil { return Block{}, err } block = Block{ Body: []Statement{ If{ Condition: stmt.Condition.Expression.Query, // Jump forward two statements, so we skip over the GOTO below that exits the WHILE loop. GotoOffset: 2, }, Goto{ // Jump forward 1 statement to get to the loop body, then jump over the loop body and the // GOTO statement that jumps to the start of the WHILE loop. Offset: 1 + OperationSizeForStatements(convertedLoopBodyStmts) + 1, }, }, Label: stmt.Label, IsLoop: true, } // Add the converted body of the WHILE loop, and a GOTO statement that jumps backwards past the current // GOTO statement, and past all the body statements, and past the GOTO statement at the start of the loop. block.Body = append(block.Body, convertedLoopBodyStmts...) block.Body = append(block.Body, Goto{Offset: -1 * (OperationSizeForStatements(convertedLoopBodyStmts) + 2)}) return block, nil }