From e27e781ceccec8b1bd72523ff0752fcde1204064 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Mon, 16 Feb 2026 16:24:32 +1100 Subject: [PATCH 1/2] feat: add J/K loop index words and nested control flow tests --- CLAUDE.md | 2 +- include/warpforth/Dialect/Forth/ForthOps.td | 17 +- .../ForthToMemRef/ForthToMemRef.cpp | 20 ++- lib/Translation/ForthToMLIR/ForthToMLIR.cpp | 12 +- .../ForthToMemRef/nested-control-flow.mlir | 149 ++++++++++++++++++ test/Pipeline/nested-control-flow.forth | 15 ++ .../Forth/nested-control-flow.forth | 83 ++++++++++ 7 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 test/Conversion/ForthToMemRef/nested-control-flow.mlir create mode 100644 test/Pipeline/nested-control-flow.forth create mode 100644 test/Translation/Forth/nested-control-flow.forth diff --git a/CLAUDE.md b/CLAUDE.md index 9755d5c..b10a33b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,7 @@ uv run ruff format gpu_test/ - **Stack Type**: `!forth.stack` - untyped stack, programmer ensures type safety - **Operations**: All take stack as input and produce stack as output (except `forth.stack`) -- **Supported Words**: literals, `DUP DROP SWAP OVER ROT NIP TUCK PICK ROLL`, `+ - * / MOD`, `AND OR XOR NOT LSHIFT RSHIFT`, `= < > <> <= >= 0=`, `@ !`, `CELLS`, `IF ELSE THEN`, `BEGIN UNTIL`, `BEGIN WHILE REPEAT`, `DO LOOP I`, `TID-X/Y/Z BID-X/Y/Z BDIM-X/Y/Z GDIM-X/Y/Z GLOBAL-ID` (GPU indexing). +- **Supported Words**: literals, `DUP DROP SWAP OVER ROT NIP TUCK PICK ROLL`, `+ - * / MOD`, `AND OR XOR NOT LSHIFT RSHIFT`, `= < > <> <= >= 0=`, `@ !`, `CELLS`, `IF ELSE THEN`, `BEGIN UNTIL`, `BEGIN WHILE REPEAT`, `DO LOOP I J K`, `TID-X/Y/Z BID-X/Y/Z BDIM-X/Y/Z GDIM-X/Y/Z GLOBAL-ID` (GPU indexing). - **Kernel Parameters**: Declared with `PARAM `, each becomes a `memref` function argument with `forth.param_name` attribute. Using a param name in code pushes its byte address onto the stack via `forth.param_ref` - **Conversion**: `!forth.stack` → `memref<256xi64>` with explicit stack pointer - **GPU**: Functions wrapped in `gpu.module`, `main` gets `gpu.kernel` attribute, configured with bare pointers for NVVM conversion diff --git a/include/warpforth/Dialect/Forth/ForthOps.td b/include/warpforth/Dialect/Forth/ForthOps.td index 973fba6..79ed571 100644 --- a/include/warpforth/Dialect/Forth/ForthOps.td +++ b/include/warpforth/Dialect/Forth/ForthOps.td @@ -529,9 +529,20 @@ def Forth_BeginWhileRepeatOp : Forth_Op<"begin_while_repeat", let hasCustomAssemblyFormat = 1; } -def Forth_LoopIndexOp : Forth_StackOpBase<"loop_index"> { - let summary = "Push loop index onto stack (I word)"; - let description = [{ Only valid inside a forth.do_loop body. ( -- i ) }]; +def Forth_LoopIndexOp : Forth_Op<"loop_index", [Pure]> { + let summary = "Push loop index onto stack (I/J/K words)"; + let description = [{ + Pushes the loop index at the given nesting depth onto the stack. + depth=0 is I (innermost), depth=1 is J, depth=2 is K. + Only valid inside nested forth.do_loop bodies at sufficient depth. + ( -- index ) + }]; + let arguments = (ins Forth_StackType:$input_stack, + DefaultValuedAttr:$depth); + let results = (outs Forth_StackType:$output_stack); + let assemblyFormat = [{ + $input_stack attr-dict `:` type($input_stack) `->` type($output_stack) + }]; } //===----------------------------------------------------------------------===// diff --git a/lib/Conversion/ForthToMemRef/ForthToMemRef.cpp b/lib/Conversion/ForthToMemRef/ForthToMemRef.cpp index ed2e2fd..706af42 100644 --- a/lib/Conversion/ForthToMemRef/ForthToMemRef.cpp +++ b/lib/Conversion/ForthToMemRef/ForthToMemRef.cpp @@ -1071,13 +1071,23 @@ struct LoopIndexOpConversion : public OpConversionPattern { Value memref = inputStack[0]; Value stackPtr = inputStack[1]; - // Find enclosing scf.for - auto forOp = op->getParentOfType(); - if (!forOp) - return rewriter.notifyMatchFailure(op, "not inside an scf.for"); + // Get the depth attribute (default 0 = I) + int64_t depth = op.getDepth(); + + // Walk up through enclosing scf.for ops + Operation *current = op.getOperation(); + scf::ForOp targetFor; + for (int64_t i = 0; i <= depth; ++i) { + targetFor = current->getParentOfType(); + if (!targetFor) + return rewriter.notifyMatchFailure( + op, "not enough enclosing scf.for ops for depth " + + std::to_string(depth)); + current = targetFor.getOperation(); + } // Get induction variable and cast index to i64 - Value iv = forOp.getInductionVar(); + Value iv = targetFor.getInductionVar(); Value ivI64 = rewriter.create(loc, rewriter.getI64Type(), iv); diff --git a/lib/Translation/ForthToMLIR/ForthToMLIR.cpp b/lib/Translation/ForthToMLIR/ForthToMLIR.cpp index c2a5d8c..c2e79ff 100644 --- a/lib/Translation/ForthToMLIR/ForthToMLIR.cpp +++ b/lib/Translation/ForthToMLIR/ForthToMLIR.cpp @@ -356,12 +356,16 @@ Value ForthParser::emitOperation(StringRef word, Value inputStack, } else if (word == "0=") { return builder.create(loc, stackType, inputStack) .getResult(); - } else if (word == "I") { - if (doLoopDepth == 0) { - (void)emitError("'I' used outside of DO/LOOP"); + } else if (word == "I" || word == "J" || word == "K") { + int64_t depth = (word == "I") ? 0 : (word == "J") ? 1 : 2; + if (doLoopDepth < depth + 1) { + (void)emitError("'" + word.str() + "' requires " + + std::to_string(depth + 1) + " nested DO/LOOP(s)"); return nullptr; } - return builder.create(loc, stackType, inputStack) + return builder + .create(loc, stackType, inputStack, + builder.getI64IntegerAttr(depth)) .getResult(); } diff --git a/test/Conversion/ForthToMemRef/nested-control-flow.mlir b/test/Conversion/ForthToMemRef/nested-control-flow.mlir new file mode 100644 index 0000000..f7face6 --- /dev/null +++ b/test/Conversion/ForthToMemRef/nested-control-flow.mlir @@ -0,0 +1,149 @@ +// RUN: %warpforth-opt --convert-forth-to-memref %s | %FileCheck %s + +// === Nested IF → nested scf.if === +// CHECK-LABEL: func.func private @test_nested_if +// CHECK: scf.if %{{.*}} -> (index) { +// CHECK: scf.if %{{.*}} -> (index) { +// CHECK: scf.yield +// CHECK: } else { +// CHECK: scf.yield +// CHECK: } +// CHECK: scf.yield +// CHECK: } else { +// CHECK: scf.yield +// CHECK: } + +module { + func.func private @test_nested_if() { + %0 = forth.stack !forth.stack + %1 = forth.literal %0 1 : !forth.stack -> !forth.stack + %2 = forth.if %1 : !forth.stack -> !forth.stack { + ^bb0(%arg0: !forth.stack): + %3 = forth.drop %arg0 : !forth.stack -> !forth.stack + %4 = forth.literal %3 1 : !forth.stack -> !forth.stack + %5 = forth.if %4 : !forth.stack -> !forth.stack { + ^bb0(%arg1: !forth.stack): + %6 = forth.drop %arg1 : !forth.stack -> !forth.stack + %7 = forth.literal %6 42 : !forth.stack -> !forth.stack + forth.yield %7 : !forth.stack + } else { + ^bb0(%arg1: !forth.stack): + %6 = forth.drop %arg1 : !forth.stack -> !forth.stack + forth.yield %6 : !forth.stack + } + forth.yield %5 : !forth.stack + } else { + ^bb0(%arg0: !forth.stack): + %3 = forth.drop %arg0 : !forth.stack -> !forth.stack + forth.yield %3 : !forth.stack + } + return + } + + // === IF inside DO/LOOP → scf.if inside scf.for === + // CHECK-LABEL: func.func private @test_if_inside_do + // CHECK: scf.for %{{.*}} = %{{.*}} to %{{.*}} step %{{.*}} iter_args(%{{.*}} = %{{.*}}) -> (index) { + // CHECK: scf.if %{{.*}} -> (index) { + // CHECK: scf.yield + // CHECK: } else { + // CHECK: scf.yield + // CHECK: } + // CHECK: scf.yield + // CHECK: } + + func.func private @test_if_inside_do() { + %0 = forth.stack !forth.stack + %1 = forth.literal %0 10 : !forth.stack -> !forth.stack + %2 = forth.literal %1 0 : !forth.stack -> !forth.stack + %3 = forth.do_loop %2 : !forth.stack -> !forth.stack { + ^bb0(%arg0: !forth.stack): + %4 = forth.loop_index %arg0 : !forth.stack -> !forth.stack + %5 = forth.literal %4 5 : !forth.stack -> !forth.stack + %6 = forth.gt %5 : !forth.stack -> !forth.stack + %7 = forth.if %6 : !forth.stack -> !forth.stack { + ^bb0(%arg1: !forth.stack): + %8 = forth.drop %arg1 : !forth.stack -> !forth.stack + %9 = forth.literal %8 99 : !forth.stack -> !forth.stack + forth.yield %9 : !forth.stack + } else { + ^bb0(%arg1: !forth.stack): + %8 = forth.drop %arg1 : !forth.stack -> !forth.stack + forth.yield %8 : !forth.stack + } + forth.yield %7 : !forth.stack + } + return + } + + // === Nested DO/LOOP with J (depth=1) === + // CHECK-LABEL: func.func private @test_nested_do_j + // Outer scf.for + // CHECK: scf.for %[[OUTER_IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}} iter_args(%{{.*}} = %{{.*}}) -> (index) { + // Inner scf.for + // CHECK: scf.for %[[INNER_IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}} iter_args(%{{.*}} = %{{.*}}) -> (index) { + // J pushes outer IV, I pushes inner IV + // CHECK: arith.index_cast %[[OUTER_IV]] + // CHECK: arith.index_cast %[[INNER_IV]] + // CHECK: scf.yield + // CHECK: } + // CHECK: scf.yield + // CHECK: } + + func.func private @test_nested_do_j() { + %0 = forth.stack !forth.stack + %1 = forth.literal %0 3 : !forth.stack -> !forth.stack + %2 = forth.literal %1 0 : !forth.stack -> !forth.stack + %3 = forth.do_loop %2 : !forth.stack -> !forth.stack { + ^bb0(%arg0: !forth.stack): + %4 = forth.literal %arg0 4 : !forth.stack -> !forth.stack + %5 = forth.literal %4 0 : !forth.stack -> !forth.stack + %6 = forth.do_loop %5 : !forth.stack -> !forth.stack { + ^bb0(%arg1: !forth.stack): + %7 = forth.loop_index %arg1 {depth = 1 : i64} : !forth.stack -> !forth.stack + %8 = forth.loop_index %7 : !forth.stack -> !forth.stack + %9 = forth.add %8 : !forth.stack -> !forth.stack + forth.yield %9 : !forth.stack + } + forth.yield %6 : !forth.stack + } + return + } + + // === BEGIN/WHILE/REPEAT inside IF === + // CHECK-LABEL: func.func private @test_while_inside_if + // CHECK: scf.if %{{.*}} -> (index) { + // CHECK: scf.while + // CHECK: scf.condition + // CHECK: } do { + // CHECK: scf.yield + // CHECK: } + // CHECK: scf.yield + // CHECK: } else { + // CHECK: scf.yield + // CHECK: } + + func.func private @test_while_inside_if() { + %0 = forth.stack !forth.stack + %1 = forth.literal %0 1 : !forth.stack -> !forth.stack + %2 = forth.if %1 : !forth.stack -> !forth.stack { + ^bb0(%arg0: !forth.stack): + %3 = forth.drop %arg0 : !forth.stack -> !forth.stack + %4 = forth.begin_while_repeat %3 : !forth.stack -> !forth.stack { + ^bb0(%arg1: !forth.stack): + %5 = forth.dup %arg1 : !forth.stack -> !forth.stack + forth.yield %5 while_cond : !forth.stack + } do { + ^bb0(%arg1: !forth.stack): + %5 = forth.literal %arg1 1 : !forth.stack -> !forth.stack + %6 = forth.sub %5 : !forth.stack -> !forth.stack + forth.yield %6 : !forth.stack + } + forth.yield %4 : !forth.stack + } else { + ^bb0(%arg0: !forth.stack): + %3 = forth.drop %arg0 : !forth.stack -> !forth.stack + forth.yield %3 : !forth.stack + } + return + } +} diff --git a/test/Pipeline/nested-control-flow.forth b/test/Pipeline/nested-control-flow.forth new file mode 100644 index 0000000..3e11155 --- /dev/null +++ b/test/Pipeline/nested-control-flow.forth @@ -0,0 +1,15 @@ +\ RUN: %warpforth-translate --forth-to-mlir %s | %warpforth-opt --warpforth-pipeline | %FileCheck %s +\ RUN: %warpforth-translate --forth-to-mlir %s | %warpforth-opt --convert-forth-to-memref --convert-scf-to-cf --convert-forth-to-gpu | %FileCheck %s --check-prefix=MID + +\ Verify nested DO/LOOP with I and J through full pipeline produces gpu.binary +\ CHECK: gpu.binary @warpforth_module + +\ Verify intermediate MLIR: gpu.func with nested loop structure +\ MID: gpu.module @warpforth_module +\ MID: gpu.func @main(%arg0: memref<4xi64> {forth.param_name = "DATA"}) kernel +\ MID: cf.br +\ MID: cf.cond_br +\ MID: gpu.return + +PARAM DATA 4 +3 0 DO 4 0 DO J I + LOOP LOOP DATA 0 CELLS + ! diff --git a/test/Translation/Forth/nested-control-flow.forth b/test/Translation/Forth/nested-control-flow.forth new file mode 100644 index 0000000..e0a43bf --- /dev/null +++ b/test/Translation/Forth/nested-control-flow.forth @@ -0,0 +1,83 @@ +\ RUN: %warpforth-translate --forth-to-mlir %s | %FileCheck %s + +\ === Nested IF === +\ CHECK: %[[S0:.*]] = forth.stack +\ CHECK: %[[S1:.*]] = forth.literal %[[S0]] 1 +\ CHECK: %[[IF1:.*]] = forth.if %[[S1]] +\ CHECK: ^bb0(%[[A1:.*]]: !forth.stack): +\ CHECK: %[[D1:.*]] = forth.drop %[[A1]] +\ CHECK: %[[L2:.*]] = forth.literal %[[D1]] 2 +\ CHECK: %[[IF2:.*]] = forth.if %[[L2]] +\ CHECK: ^bb0(%[[A2:.*]]: !forth.stack): +\ CHECK: forth.drop %[[A2]] +\ CHECK: forth.literal %{{.*}} 3 +\ CHECK: forth.yield +\ CHECK: } else { +\ CHECK: ^bb0(%[[A3:.*]]: !forth.stack): +\ CHECK: forth.drop %[[A3]] +\ CHECK: forth.yield +\ CHECK: } +\ CHECK: forth.yield +\ CHECK: } else { +\ CHECK: ^bb0(%[[A4:.*]]: !forth.stack): +\ CHECK: forth.drop %[[A4]] +\ CHECK: forth.yield +\ CHECK: } +1 IF 2 IF 3 THEN THEN + +\ === IF inside DO === +\ CHECK: %[[S2:.*]] = forth.literal %[[IF1]] 10 +\ CHECK: %[[S3:.*]] = forth.literal %[[S2]] 0 +\ CHECK: %[[LOOP1:.*]] = forth.do_loop %[[S3]] +\ CHECK: ^bb0(%[[BA:.*]]: !forth.stack): +\ CHECK: %[[LI:.*]] = forth.loop_index %[[BA]] +\ CHECK: %[[L5:.*]] = forth.literal %[[LI]] 5 +\ CHECK: %[[GT:.*]] = forth.gt %[[L5]] +\ CHECK: forth.if %[[GT]] +\ CHECK: forth.loop_index +\ CHECK: forth.yield +10 0 DO I 5 > IF I THEN LOOP + +\ === Nested DO with J === +\ CHECK: forth.do_loop +\ CHECK: forth.do_loop +\ CHECK: forth.loop_index %{{.*}} {depth = 1 : i64} +\ CHECK: forth.loop_index %{{.*}} +\ CHECK: forth.add +3 0 DO 4 0 DO J I + LOOP LOOP + +\ === Triple-nested DO with K === +\ CHECK: forth.do_loop +\ CHECK: forth.do_loop +\ CHECK: forth.do_loop +\ CHECK: forth.loop_index %{{.*}} {depth = 2 : i64} +\ CHECK: forth.loop_index %{{.*}} {depth = 1 : i64} +\ CHECK: forth.loop_index %{{.*}} +\ CHECK: forth.add +\ CHECK: forth.add +2 0 DO 2 0 DO 2 0 DO K J I + + LOOP LOOP LOOP + +\ === BEGIN/WHILE inside IF === +\ CHECK: forth.if +\ CHECK: forth.begin_while_repeat +\ CHECK: forth.dup +\ CHECK: forth.yield %{{.*}} while_cond +\ CHECK: } do { +\ CHECK: forth.literal %{{.*}} 1 +\ CHECK: forth.sub +\ CHECK: forth.yield +5 IF BEGIN DUP WHILE 1 - REPEAT THEN + +\ === IF inside BEGIN/UNTIL === +\ CHECK: forth.begin_until +\ CHECK: forth.dup +\ CHECK: forth.literal %{{.*}} 10 +\ CHECK: forth.lt +\ CHECK: forth.if +\ CHECK: forth.literal %{{.*}} 1 +\ CHECK: forth.add +\ CHECK: forth.dup +\ CHECK: forth.literal %{{.*}} 20 +\ CHECK: forth.eq +\ CHECK: forth.yield +BEGIN DUP 10 < IF 1 + THEN DUP 20 = UNTIL From 6245b73fbdb8f8f11e109b1a67145284666245ab Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Mon, 16 Feb 2026 16:29:58 +1100 Subject: [PATCH 2/2] test(translation): add negative test for J/K at insufficient nesting depth --- test/Translation/Forth/loop-index-depth-error.forth | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/Translation/Forth/loop-index-depth-error.forth diff --git a/test/Translation/Forth/loop-index-depth-error.forth b/test/Translation/Forth/loop-index-depth-error.forth new file mode 100644 index 0000000..f8ad22e --- /dev/null +++ b/test/Translation/Forth/loop-index-depth-error.forth @@ -0,0 +1,3 @@ +\ RUN: %not %warpforth-translate --forth-to-mlir %s 2>&1 | %FileCheck %s +\ CHECK: 'J' requires 2 nested DO/LOOP(s) +10 0 DO J LOOP