diff --git a/include/circt/Dialect/Sim/SimOps.td b/include/circt/Dialect/Sim/SimOps.td index 58cc29619553..1f419ae2e1a6 100644 --- a/include/circt/Dialect/Sim/SimOps.td +++ b/include/circt/Dialect/Sim/SimOps.td @@ -561,10 +561,20 @@ def PrintFormattedOp : SimOp<"print"> { let arguments = (ins FormatStringType:$input, ClockType:$clock, - I1:$condition + I1:$condition, + Optional:$stream ); + let builders = [ + OpBuilder<(ins + "Value":$input, + "Value":$clock, + "Value":$condition + ), [{ + build($_builder, $_state, input, clock, condition, mlir::Value()); + }]> + ]; let hasCanonicalizeMethod = true; - let assemblyFormat = "$input `on` $clock `if` $condition attr-dict"; + let assemblyFormat = "$input `on` $clock `if` $condition (`to` $stream^)? attr-dict"; } def PrintFormattedProcOp : SimOp<"proc.print"> { @@ -574,10 +584,38 @@ def PrintFormattedProcOp : SimOp<"proc.print"> { This operation must be within a procedural region. }]; - let arguments = (ins FormatStringType:$input); + let arguments = (ins + FormatStringType:$input, + Optional:$stream + ); + let builders = [ + OpBuilder<(ins + "Value":$input + ), [{ + build($_builder, $_state, input, mlir::Value()); + }]> + ]; let hasVerifier = true; let hasCanonicalizeMethod = true; - let assemblyFormat = "$input attr-dict"; + let assemblyFormat = "$input (`to` $stream^)? attr-dict"; +} + +def GetFileOp : SimOp<"get_file", [Pure]> { + let summary = "Get an output stream for a formatted file name"; + let description = [{ + Resolves a file name from a `!sim.fstring` value and returns an output + stream resource that can be consumed by `sim.print` or `sim.proc.print`. + The file-name format string should be constructed using `sim.fmt.*` + operations, just like any other Sim format-string value. + The file-name format string is evaluated whenever the stream is accessed. + + The concrete runtime representation and lifetime management of the returned + stream are backend-defined. + }]; + + let arguments = (ins FormatStringType:$fileName); + let results = (outs OutputStreamType:$result); + let assemblyFormat = "$fileName attr-dict"; } //===----------------------------------------------------------------------===// diff --git a/include/circt/Dialect/Sim/SimTypes.td b/include/circt/Dialect/Sim/SimTypes.td index d3feeddf3ea2..3ca866e70bdc 100644 --- a/include/circt/Dialect/Sim/SimTypes.td +++ b/include/circt/Dialect/Sim/SimTypes.td @@ -111,4 +111,17 @@ def DPIFunctionType : SimTypeDef<"DPIFunction"> { }]; } +def OutputStreamType : SimTypeDef<"OutputStream"> { + let summary = "a type representing an output stream"; + let description = [{ + Represents a simulation output stream resource. + This type models the existence of a stream resource at the Sim dialect level, + while leaving its concrete representation to the lowering backend. Backends may + realize this as a file descriptor, object handle, pointer, or any other target- + specific resource representation. + }]; + + let mnemonic = "output_stream"; +} + #endif // CIRCT_DIALECT_SIM_SIMTYPES_TD diff --git a/lib/Dialect/Sim/SimOps.cpp b/lib/Dialect/Sim/SimOps.cpp index 4f3bc5423595..92dc72ff735c 100644 --- a/lib/Dialect/Sim/SimOps.cpp +++ b/lib/Dialect/Sim/SimOps.cpp @@ -527,6 +527,8 @@ LogicalResult FormatStringConcatOp::verify() { LogicalResult FormatStringConcatOp::canonicalize(FormatStringConcatOp op, PatternRewriter &rewriter) { + // Any helper literals created during canonicalization must dominate `op`. + rewriter.setInsertionPoint(op); auto fmtStrType = FormatStringType::get(op.getContext()); diff --git a/lib/Dialect/Sim/Transforms/ProceduralizeSim.cpp b/lib/Dialect/Sim/Transforms/ProceduralizeSim.cpp index d6e21796a113..84f4c16d8718 100644 --- a/lib/Dialect/Sim/Transforms/ProceduralizeSim.cpp +++ b/lib/Dialect/Sim/Transforms/ProceduralizeSim.cpp @@ -37,6 +37,81 @@ using namespace circt; using namespace sim; namespace { +static LogicalResult collectFormatStringFragments( + Value formatString, PrintFormattedOp anchorOp, + SmallVectorImpl &fragmentList, + SmallSetVector &allFStringFragments, + SmallSetVector &arguments) { + SmallVector flatString; + if (auto concatInput = formatString.getDefiningOp()) { + auto isAcyclic = concatInput.getFlattenedInputs(flatString); + if (failed(isAcyclic)) { + anchorOp.emitError("Cyclic format string cannot be proceduralized."); + return failure(); + } + } else { + flatString.push_back(formatString); + } + + assert(fragmentList.empty() && "format string visited twice"); + for (auto fragment : flatString) { + auto *fmtOp = fragment.getDefiningOp(); + if (!fmtOp) { + anchorOp.emitError("Proceduralization of format strings passed as block " + "argument is unsupported."); + return failure(); + } + fragmentList.push_back(fmtOp); + allFStringFragments.insert(fmtOp); + + // For non-literal fragments, the value to be formatted has to become a + // triggered region argument. + if (!llvm::isa(fmtOp)) { + auto fmtVal = getFormattedValue(fmtOp); + assert(!!fmtVal && "Unexpected formatting fragment op."); + arguments.insert(fmtVal); + } + } + return success(); +} + +static Value +rematerializeFormatStringFromFragments(ArrayRef fragments, + OpBuilder &builder, IRMapping &mapping, + Location loc) { + SmallVector operands; + operands.reserve(fragments.size()); + for (auto *fragment : fragments) { + auto cloned = mapping.lookupOrNull(fragment->getResult(0)); + assert(cloned && "missing cloned fragment"); + operands.push_back(cloned); + } + if (operands.size() == 1) + return operands.front(); + return builder.createOrFold(loc, operands); +} + +static Block *getOrCreateConditionBlock(OpBuilder &builder, + hw::TriggeredOp trigOp, Location loc, + Value condition, + Value &prevConditionValue, + Block *&prevConditionBlock) { + if (condition != prevConditionValue) + prevConditionBlock = nullptr; + + if (prevConditionBlock) + return prevConditionBlock; + + builder.setInsertionPointToEnd(trigOp.getBodyBlock()); + auto ifOp = mlir::scf::IfOp::create(builder, loc, TypeRange{}, condition, + true, false); + builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); + mlir::scf::YieldOp::create(builder, loc); + prevConditionValue = condition; + prevConditionBlock = builder.getBlock(); + return prevConditionBlock; +} + struct ProceduralizeSimPass : impl::ProceduralizeSimBase { public: void runOnOperation() override; @@ -44,7 +119,6 @@ struct ProceduralizeSimPass : impl::ProceduralizeSimBase { private: LogicalResult proceduralizePrintOps(Value clock, ArrayRef printOps); - SmallVector getPrintFragments(PrintFormattedOp op); void cleanup(); // Mapping Clock -> List of printf ops @@ -60,67 +134,66 @@ LogicalResult ProceduralizeSimPass::proceduralizePrintOps( // List of uniqued values to become arguments of the TriggeredOp. SmallSetVector arguments; - // Map printf ops -> flattened list of fragments - SmallDenseMap, 4> fragmentMap; + // Map print ops -> flattened list of format-string fragments. + SmallDenseMap, 4> printFragmentMap; + // Map get_file ops -> flattened list of filename format-string fragments. + SmallDenseMap, 4> fileNameFragmentMap; + // All non-concat format-string fragment ops needed in the triggered body. + SmallSetVector allFStringFragments; + // Keep get_file ops in first-use order. + SmallSetVector getFileOps; SmallVector locs; SmallDenseSet alwaysEnabledConditions; + SmallVector livePrintOps; locs.reserve(printOps.size()); - for (auto printOp : printOps) { - // Handle the print condition value. If it is not constant, it has to become - // a region argument. If it is constant false, skip the operation. if (auto cstCond = printOp.getCondition().getDefiningOp()) { + if (cstCond.getValue().isZero()) { + printOp.erase(); + continue; + } if (cstCond.getValue().isAllOnes()) alwaysEnabledConditions.insert(printOp.getCondition()); - else - continue; } else { arguments.insert(printOp.getCondition()); } - // Accumulate locations + livePrintOps.push_back(printOp); locs.push_back(printOp.getLoc()); - // Get the flat list of formatting fragments and collect leaf fragments - SmallVector flatString; - if (auto concatInput = - printOp.getInput().getDefiningOp()) { - - auto isAcyclic = concatInput.getFlattenedInputs(flatString); - if (failed(isAcyclic)) { - printOp.emitError("Cyclic format string cannot be proceduralized."); + auto &printFragments = printFragmentMap[printOp]; + if (failed(::collectFormatStringFragments(printOp.getInput(), printOp, + printFragments, + allFStringFragments, arguments))) + return failure(); + + if (auto stream = printOp.getStream()) { + auto getFileOp = stream.getDefiningOp(); + if (!getFileOp) { + if (!stream.getDefiningOp()) + printOp.emitError("proceduralization requires stream to be produced " + "by sim.get_file, block arguments are unsupported"); + else + printOp.emitError("proceduralization requires stream to be produced " + "by sim.get_file"); return failure(); } - } else { - flatString.push_back(printOp.getInput()); - } - - auto &fragmentList = fragmentMap[printOp]; - assert(fragmentList.empty() && "printf operation visited twice."); - - for (auto &fragment : flatString) { - auto *fmtOp = fragment.getDefiningOp(); - if (!fmtOp) { - printOp.emitError("Proceduralization of format strings passed as block " - "argument is unsupported."); + getFileOps.insert(getFileOp); + auto &fileNameFragments = fileNameFragmentMap[getFileOp]; + if (fileNameFragments.empty() && + failed(::collectFormatStringFragments( + getFileOp.getFileName(), printOp, fileNameFragments, + allFStringFragments, arguments))) return failure(); - } - fragmentList.push_back(fmtOp); - // For non-literal fragments, the value to be formatted has to become an - // argument. - if (!llvm::isa(fmtOp)) { - auto fmtVal = getFormattedValue(fmtOp); - assert(!!fmtVal && "Unexpected foramtting fragment op."); - arguments.insert(fmtVal); - } } } - // Build the hw::TriggeredOp - OpBuilder builder(printOps.back()); - auto fusedLoc = builder.getFusedLoc(locs); + if (livePrintOps.empty()) + return success(); + OpBuilder builder(livePrintOps.back()); + auto fusedLoc = builder.getFusedLoc(locs); SmallVector argVec = arguments.takeVector(); auto clockConv = builder.createOrFold(fusedLoc, clock); @@ -130,78 +203,73 @@ LogicalResult ProceduralizeSimPass::proceduralizePrintOps( hw::EventControl::AtPosEdge), clockConv, argVec); - // Map the collected arguments to the newly created block arguments. - IRMapping argumentMapper; - unsigned idx = 0; - for (auto arg : argVec) { - argumentMapper.map(arg, trigOp.getBodyBlock()->getArgument(idx)); - idx++; - } + IRMapping mapping; + for (auto [idx, arg] : llvm::enumerate(argVec)) + mapping.map(arg, trigOp.getBodyBlock()->getArgument(idx)); - // Materialize and map a 'true' constant within the TriggeredOp if required. builder.setInsertionPointToStart(trigOp.getBodyBlock()); if (!alwaysEnabledConditions.empty()) { auto cstTrue = builder.createOrFold( fusedLoc, IntegerAttr::get(builder.getI1Type(), 1)); for (auto cstCond : alwaysEnabledConditions) - argumentMapper.map(cstCond, cstTrue); + mapping.map(cstCond, cstTrue); + } + + for (auto *fragment : allFStringFragments) { + auto original = fragment->getResult(0); + if (mapping.lookupOrNull(original)) + continue; + auto *cloned = builder.clone(*fragment, mapping); + mapping.map(original, cloned->getResult(0)); + } + + for (auto getFileOp : getFileOps) { + auto &fileNameFragments = fileNameFragmentMap[getFileOp]; + Value clonedFileName = ::rematerializeFormatStringFromFragments( + fileNameFragments, builder, mapping, getFileOp.getLoc()); + + auto clonedGetFile = + GetFileOp::create(builder, getFileOp.getLoc(), clonedFileName); + mapping.map(getFileOp.getResult(), clonedGetFile.getResult()); + + cleanupList.push_back(getFileOp); + cleanupList.push_back(getFileOp.getFileName().getDefiningOp()); + } + + // Materialize print inputs before creating any conditional blocks. + // Whether to actually construct strings eagerly/lazily is left to lowering + // backends. + SmallDenseMap procPrintInputMap; + // Insert after rematerialized fragments/get_file ops so operands dominate. + builder.setInsertionPointToEnd(trigOp.getBodyBlock()); + for (auto printOp : livePrintOps) { + auto &printFragments = printFragmentMap[printOp]; + procPrintInputMap[printOp] = ::rematerializeFormatStringFromFragments( + printFragments, builder, mapping, printOp.getLoc()); } - SmallDenseMap cloneMap; Value prevConditionValue; - Block *prevConditionBlock; + Block *prevConditionBlock = nullptr; + for (auto printOp : livePrintOps) { + auto condArg = mapping.lookup(printOp.getCondition()); + auto *condBlock = + ::getOrCreateConditionBlock(builder, trigOp, printOp.getLoc(), condArg, + prevConditionValue, prevConditionBlock); - for (auto printOp : printOps) { + builder.setInsertionPoint(condBlock->getTerminator()); + Value procPrintInput = procPrintInputMap[printOp]; - // Throw away disabled prints - if (auto cstCond = printOp.getCondition().getDefiningOp()) { - if (cstCond.getValue().isZero()) { - printOp.erase(); - continue; + Value procPrintStream; + if (auto stream = printOp.getStream()) { + procPrintStream = mapping.lookupOrNull(stream); + if (!procPrintStream) { + printOp.emitError("proceduralization failed to rematerialize stream"); + return failure(); } } - // Create a copy of the required fragment operations within the - // TriggeredOp's body. - auto fragments = fragmentMap[printOp]; - SmallVector clonedOperands; - builder.setInsertionPointToStart(trigOp.getBodyBlock()); - for (auto *fragment : fragments) { - auto &fmtCloned = cloneMap[fragment]; - if (!fmtCloned) - fmtCloned = builder.clone(*fragment, argumentMapper); - clonedOperands.push_back(fmtCloned->getResult(0)); - } - // Concatenate fragments to a single value if necessary. - Value procPrintInput; - if (clonedOperands.size() != 1) - procPrintInput = builder.createOrFold( - printOp.getLoc(), clonedOperands); - else - procPrintInput = clonedOperands.front(); - - // Check if we can reuse the previous conditional block. - auto condArg = argumentMapper.lookup(printOp.getCondition()); - if (condArg != prevConditionValue) - prevConditionBlock = nullptr; - auto *condBlock = prevConditionBlock; - - // If not, create a new scf::IfOp for the condition. - if (!condBlock) { - builder.setInsertionPointToEnd(trigOp.getBodyBlock()); - auto ifOp = mlir::scf::IfOp::create(builder, printOp.getLoc(), - TypeRange{}, condArg, true, false); - builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); - mlir::scf::YieldOp::create(builder, printOp.getLoc()); - condBlock = builder.getBlock(); - prevConditionValue = condArg; - prevConditionBlock = condBlock; - } - - // Create the procedural print operation and prune the operations outside of - // the TriggeredOp. - builder.setInsertionPoint(condBlock->getTerminator()); - PrintFormattedProcOp::create(builder, printOp.getLoc(), procPrintInput); + PrintFormattedProcOp::create(builder, printOp.getLoc(), procPrintInput, + procPrintStream); cleanupList.push_back(printOp.getInput().getDefiningOp()); printOp.erase(); } diff --git a/test/Dialect/Sim/proceduralize-sim-errors.mlir b/test/Dialect/Sim/proceduralize-sim-errors.mlir index 907e0187b5b5..43a48138930d 100644 --- a/test/Dialect/Sim/proceduralize-sim-errors.mlir +++ b/test/Dialect/Sim/proceduralize-sim-errors.mlir @@ -7,3 +7,14 @@ hw.module @cyclic_concat(in %clk : !seq.clock) { // expected-error @below {{Cyclic format string cannot be proceduralized.}} sim.print %ping on %clk if %true } + +// ----- + +hw.module @stream_block_arg_unsupported( + in %clk : !seq.clock, + in %en : i1, + in %stream : !sim.output_stream) { + %msg = sim.fmt.literal "x" + // expected-error @below {{proceduralization requires stream to be produced by sim.get_file, block arguments are unsupported}} + sim.print %msg on %clk if %en to %stream +} diff --git a/test/Dialect/Sim/proceduralize-sim.mlir b/test/Dialect/Sim/proceduralize-sim.mlir index 2e46bf55222d..c1d1a905755b 100644 --- a/test/Dialect/Sim/proceduralize-sim.mlir +++ b/test/Dialect/Sim/proceduralize-sim.mlir @@ -262,3 +262,84 @@ hw.module @condition_as_val(in %clk: !seq.clock, in %condval: i1) { %bin = sim.fmt.bin %condval specifierWidth 1 : i1 sim.print %bin on %clk if %condval } + +// CHECK-LABEL: @print_with_stream +// CHECK-NEXT: %[[TRG:.*]] = seq.from_clock %clk +// CHECK-NEXT: hw.triggered posedge %[[TRG]](%cond, %idx) : i1, i32 { +// CHECK-NEXT: ^bb0(%[[ARGCOND:.*]]: i1, %[[ARGIDX:.*]]: i32): +// CHECK-DAG: %[[FMT:.*]] = sim.fmt.literal "stream" +// CHECK-DAG: %[[PFX:.*]] = sim.fmt.literal "out_" +// CHECK-DAG: %[[NUM:.*]] = sim.fmt.dec %[[ARGIDX]] : i32 +// CHECK-DAG: %[[SFX:.*]] = sim.fmt.literal ".log" +// CHECK-DAG: %[[FNAME:.*]] = sim.fmt.concat (%[[PFX]], %[[NUM]], %[[SFX]]) +// CHECK-DAG: %[[FILE:.*]] = sim.get_file %[[FNAME]] +// CHECK: scf.if %[[ARGCOND]] { +// CHECK-NEXT: sim.proc.print %[[FMT]] to %[[FILE]] +// CHECK-NEXT: } +// CHECK-NEXT: } +hw.module @print_with_stream(in %clk: !seq.clock, in %cond: i1, in %idx: i32) { + %fmt = sim.fmt.literal "stream" + %prefix = sim.fmt.literal "out_" + %idxFmt = sim.fmt.dec %idx : i32 + %suffix = sim.fmt.literal ".log" + %fileName = sim.fmt.concat (%prefix, %idxFmt, %suffix) + %file = sim.get_file %fileName + sim.print %fmt on %clk if %cond to %file +} + +// CHECK-LABEL: @print_with_stream_two_conditions +// CHECK-NEXT: %[[TRG:.*]] = seq.from_clock %clk +// CHECK-NEXT: hw.triggered posedge %[[TRG]](%c1, %idx, %c2) : i1, i32, i1 { +// CHECK-NEXT: ^bb0(%[[ARGC1:.*]]: i1, %[[ARGIDX:.*]]: i32, %[[ARGC2:.*]]: i1): +// CHECK-DAG: %[[FMT:.*]] = sim.fmt.literal "stream2" +// CHECK-DAG: %[[PFX:.*]] = sim.fmt.literal "out2_" +// CHECK-DAG: %[[NUM:.*]] = sim.fmt.dec %[[ARGIDX]] : i32 +// CHECK-DAG: %[[SFX:.*]] = sim.fmt.literal ".log" +// CHECK-DAG: %[[FNAME:.*]] = sim.fmt.concat (%[[PFX]], %[[NUM]], %[[SFX]]) +// CHECK-DAG: %[[FILE:.*]] = sim.get_file %[[FNAME]] +// CHECK: scf.if %[[ARGC1]] { +// CHECK-NEXT: sim.proc.print %[[FMT]] to %[[FILE]] +// CHECK-NEXT: } +// CHECK-NEXT: scf.if %[[ARGC2]] { +// CHECK-NEXT: sim.proc.print %[[FMT]] to %[[FILE]] +// CHECK-NEXT: } +// CHECK-NEXT: } +hw.module @print_with_stream_two_conditions(in %clk: !seq.clock, in %c1: i1, in %c2: i1, in %idx: i32) { + %fmt = sim.fmt.literal "stream2" + %prefix = sim.fmt.literal "out2_" + %idxFmt = sim.fmt.dec %idx : i32 + %suffix = sim.fmt.literal ".log" + %fileName = sim.fmt.concat (%prefix, %idxFmt, %suffix) + %file = sim.get_file %fileName + sim.print %fmt on %clk if %c1 to %file + sim.print %fmt on %clk if %c2 to %file +} + +// CHECK-LABEL: @shared_fmt_between_print_and_get_file +// CHECK-NEXT: %[[TRG:.*]] = seq.from_clock %clk +// CHECK-NEXT: hw.triggered posedge %[[TRG]](%cond, %idx) : i1, i32 { +// CHECK-NEXT: ^bb0(%[[ARGCOND:.*]]: i1, %[[ARGIDX:.*]]: i32): +// CHECK-DAG: %[[SHARED:.*]] = sim.fmt.dec %[[ARGIDX]] : i32 +// CHECK-DAG: %[[MSGPFX:.*]] = sim.fmt.literal "value=" +// CHECK-DAG: %[[FILEPFX:.*]] = sim.fmt.literal "out_" +// CHECK-DAG: %[[FILESFX:.*]] = sim.fmt.literal ".log" +// CHECK-DAG: %[[MSG:.*]] = sim.fmt.concat (%[[MSGPFX]], %[[SHARED]]) +// CHECK-DAG: %[[FNAME:.*]] = sim.fmt.concat (%[[FILEPFX]], %[[SHARED]], %[[FILESFX]]) +// CHECK-DAG: %[[FILE:.*]] = sim.get_file %[[FNAME]] +// CHECK: scf.if %[[ARGCOND]] { +// CHECK-NEXT: sim.proc.print %[[MSG]] to %[[FILE]] +// CHECK-NEXT: } +// CHECK-NEXT: } +hw.module @shared_fmt_between_print_and_get_file( + in %clk: !seq.clock, in %cond: i1, in %idx: i32) { + %shared = sim.fmt.dec %idx : i32 + %msgPrefix = sim.fmt.literal "value=" + %msg = sim.fmt.concat (%msgPrefix, %shared) + + %filePrefix = sim.fmt.literal "out_" + %fileSuffix = sim.fmt.literal ".log" + %fileName = sim.fmt.concat (%filePrefix, %shared, %fileSuffix) + %file = sim.get_file %fileName + + sim.print %msg on %clk if %cond to %file +} diff --git a/test/Dialect/Sim/round-trip.mlir b/test/Dialect/Sim/round-trip.mlir index 65b34115dd89..3040e0d4af92 100644 --- a/test/Dialect/Sim/round-trip.mlir +++ b/test/Dialect/Sim/round-trip.mlir @@ -99,3 +99,22 @@ func.func @DynamicStrings(%idx: i32) { %char = sim.string.get %str[%idx] return } + +hw.module @PrintFormattedWithStream(in %clock: !seq.clock, in %condition: i1, in %idx: i32) { + // CHECK: %[[FMT:.*]] = sim.fmt.literal "literal string" + %str = sim.fmt.literal "literal string" + // CHECK: %[[FN0:.*]] = sim.fmt.literal "output_" + %fn0 = sim.fmt.literal "output_" + // CHECK: %[[FN1:.*]] = sim.fmt.dec %idx : i32 + %fn1 = sim.fmt.dec %idx : i32 + // CHECK: %[[FN2:.*]] = sim.fmt.literal ".txt" + %fn2 = sim.fmt.literal ".txt" + // CHECK: %[[FNAME:.*]] = sim.fmt.concat (%[[FN0]], %[[FN1]], %[[FN2]]) + %fileName = sim.fmt.concat (%fn0, %fn1, %fn2) + // CHECK: %[[FILE:.*]] = sim.get_file %[[FNAME]] + %file = sim.get_file %fileName + // CHECK: sim.print %[[FMT]] on %clock if %condition + sim.print %str on %clock if %condition + // CHECK: sim.print %[[FMT]] on %clock if %condition to %[[FILE]] + sim.print %str on %clock if %condition to %file +}