Skip to content

Commit

Permalink
Add support for generic intrinsic expressions and statements. (#3986)
Browse files Browse the repository at this point in the history
  • Loading branch information
dtzSiFive authored and SpriteOvO committed Apr 20, 2024
1 parent fabde87 commit 87d4d45
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 18 deletions.
61 changes: 61 additions & 0 deletions core/src/main/scala/chisel3/Intrinsic.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0

package chisel3

import chisel3._
import chisel3.experimental.{requireIsChiselType, Param, SourceInfo}
import chisel3.internal.firrtl.ir._
import chisel3.internal.{Builder, OpBinding}
import chisel3.internal.Builder.pushCommand

object Intrinsic {

/** Create an intrinsic statement.
*
* @param intrinsic name of the intrinsic
* @param params parameter name/value pairs, if any. Parameter names must be unique.
* @param data inputs
*
* @example {{{
* Intrinsic("test", "Foo" -> 5)(f, g)
* }}}
*/
def apply(intrinsic: String, params: (String, Param)*)(data: Data*)(implicit sourceInfo: SourceInfo): Unit = {
require(params.map(_._1).distinct.size == params.size, "parameter names must be unique")
pushCommand(DefIntrinsic(sourceInfo, intrinsic, data.map(_.ref), params))
}
}

object IntrinsicExpr {

/** Create an intrinsic expression.
*
* @param intrinsic name of the intrinsic
* @param ret return type of the expression
* @param params parameter name/value pairs, if any. Parameter names must be unique.
* @param data inputs
* @return intrinsic expression that returns the specified return type
*
* @example {{{
* val test = IntrinsicExpr("test", UInt(32.W), "Foo" -> 5)(f, g) + 3.U
* }}}
*/
def apply[T <: Data](
intrinsic: String,
ret: => T,
params: (String, Param)*
)(data: Data*
)(
implicit sourceInfo: SourceInfo
): T = {
val prevId = Builder.idGen.value
val t = ret // evaluate once (passed by name)
requireIsChiselType(t, "intrinsic type")
val int = if (!t.mustClone(prevId)) t else t.cloneTypeFull

int.bind(OpBinding(Builder.forcedUserModule, Builder.currentWhen))
require(params.map(_._1).distinct.size == params.size, "parameter names must be unique")
pushCommand(DefIntrinsicExpr(sourceInfo, intrinsic, int, data.map(_.ref), params))
int
}
}
18 changes: 18 additions & 0 deletions core/src/main/scala/chisel3/internal/firrtl/Converter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,24 @@ private[chisel3] object Converter {
e.name
)
)
case i @ DefIntrinsic(info, intrinsic, args, params) =>
Some(
fir.IntrinsicStmt(
convert(info),
intrinsic,
args.map(a => convert(a, ctx, info)),
params.map { case (k, v) => convert(k, v) }
)
)
case i @ DefIntrinsicExpr(info, intrinsic, id, args, params) =>
val tpe = extractType(id, info, typeAliases)
val expr = fir.IntrinsicExpr(
intrinsic,
args.map(a => convert(a, ctx, info)),
params.map { case (k, v) => convert(k, v) },
tpe
)
Some(fir.DefNode(convert(info), i.name, expr))
case _ => None
}

Expand Down
11 changes: 11 additions & 0 deletions core/src/main/scala/chisel3/internal/firrtl/IR.scala
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,17 @@ private[chisel3] object ir {
params: Map[String, Param])
extends Component

case class DefIntrinsicExpr[T <: Data](
sourceInfo: SourceInfo,
intrinsic: String,
id: T,
args: Seq[Arg],
params: Seq[(String, Param)])
extends Definition

case class DefIntrinsic(sourceInfo: SourceInfo, intrinsic: String, args: Seq[Arg], params: Seq[(String, Param)])
extends Command

case class DefClass(id: Class, name: String, ports: Seq[Port], commands: Seq[Command]) extends Component

case class Circuit(
Expand Down
32 changes: 25 additions & 7 deletions docs/src/explanations/intrinsics.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,44 @@ section: "chisel3"

# Intrinsics

Chisel *Intrinsics* are used to instantiate implementation defined functionality.
Chisel *Intrinsics* are used to express implementation defined functionality.
Intrinsics provide a way for specific compilers to extend the capabilities of
the language in ways which are not implementable with library code.

Intrinsics will be typechecked by the implementation. What intrinsics are
available is documented by an implementation.

The `Intrinsic` and `IntrinsicExpr` can be used to create intrinsic statements
and expressions.

Modules defined as an `IntrinsicModule` will be instantiated as normal modules,
but the intrinsic field communicates to the compiler what functionality to use to
implement the module. Implementations may not be actual modules, the module
nature of intrinsics is merely for instantiation purposes.

Intrinsics will be typechecked by the implementation. What intrinsics are
available is documented by an implementation.

### Parameterization

Parameters can be passed as an argument to the IntModule constructor.

### Example
### Intrinsic Expression Example

This following creates an intrinsic for the intrinsic named "MyIntrinsic".
It takes a parameter named "STRING" and has several inputs.

```scala mdoc:invisible
import chisel3._
```

```scala mdoc:compile-only
class Foo extends RawModule {
val myresult = IntrinsicExpr("MyIntrinsic", UInt(32.W), "STRING" -> "test")(3.U, 5.U)
}
```

### IntrinsicModule Example

This following creates an intrinsic module for the intrinsic named
"OtherIntrinsic". It takes a parameter named "STRING" and has several ports.
This following creates an intrinsic module for the intrinsic named
"OtherIntrinsic". It takes a parameter named "STRING" and has one bundle port.

```scala mdoc:invisible
import chisel3._
Expand Down
17 changes: 17 additions & 0 deletions firrtl/src/main/scala/firrtl/ir/IR.scala
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,23 @@ case class LayerBlock(info: Info, layer: String, body: Statement) extends Statem
case class DefOption(info: Info, name: String, cases: Seq[DefOptionCase])
case class DefOptionCase(info: Info, name: String)

case class IntrinsicExpr(
intrinsic: String,
args: Seq[Expression],
params: Seq[Param],
tpe: Type)
extends Expression
with UseSerializer

case class IntrinsicStmt(
info: Info,
intrinsic: String,
args: Seq[Expression],
params: Seq[Param],
tpe: Option[Type] = None)
extends Statement
with UseSerializer

// formal
object Formal extends Enumeration {
val Assert = Value("assert")
Expand Down
58 changes: 47 additions & 11 deletions firrtl/src/main/scala/firrtl/ir/Serializer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,11 @@ object Serializer {
if (idx != lastIdx) b ++= ", "
}
b += ')'
case ProbeExpr(expr, _) => b ++= "probe("; s(expr); b += ')'
case RWProbeExpr(expr, _) => b ++= "rwprobe("; s(expr); b += ')'
case ProbeRead(expr, _) => b ++= "read("; s(expr); b += ')'
case other => b ++= other.serialize // Handle user-defined nodes
case ProbeExpr(expr, _) => b ++= "probe("; s(expr); b += ')'
case RWProbeExpr(expr, _) => b ++= "rwprobe("; s(expr); b += ')'
case ProbeRead(expr, _) => b ++= "read("; s(expr); b += ')'
case IntrinsicExpr(intrinsic, args, params, tpe) => sIntrinsic(NoInfo, intrinsic, args, params, Some(tpe))
case other => b ++= other.serialize // Handle user-defined nodes
}

// Helper for some not-real Statements that only exist for Serialization
Expand Down Expand Up @@ -354,13 +355,48 @@ object Serializer {
b ++= "force("; s(clock); b ++= ", "; s(cond); b ++= ", "; s(probe); b ++= ", "; s(value); b += ')'; s(info)
case ProbeRelease(info, clock, cond, probe) =>
b ++= "release("; s(clock); b ++= ", "; s(cond); b ++= ", "; s(probe); b += ')'; s(info)
case other => b ++= other.serialize // Handle user-defined nodes
case IntrinsicStmt(info, intrinsic, args, params, tpe) => sIntrinsic(info, intrinsic, args, params, tpe)
case other => b ++= other.serialize // Handle user-defined nodes
}

private def sStmtName(lbl: String)(implicit b: StringBuilder): Unit = {
if (lbl.nonEmpty) { b ++= s" : ${legalize(lbl)}" }
}

private def sIntrinsic(
info: Info,
intrinsic: String,
args: Seq[Expression],
params: Seq[Param],
tpe: Option[Type]
)(
implicit b: StringBuilder,
indent: Int
): Unit = {
b ++= "intrinsic("
b ++= intrinsic
if (params.nonEmpty) {
b += '<';
val lastIdx = params.size - 1
params.zipWithIndex.foreach {
case (param, idx) =>
s(param)
if (idx != lastIdx) b ++= ", "
}
b += '>'
}
if (tpe.nonEmpty) {
b ++= " : "
s(tpe.get)
}
if (args.nonEmpty) {
b ++= ", "
s(args, ", ")
}
b += ')'
s(info)
}

private def s(node: Width)(implicit b: StringBuilder, indent: Int): Unit = node match {
case IntWidth(width) => b += '<'; b ++= width.toString(); b += '>'
case UnknownWidth => // empty string
Expand Down Expand Up @@ -437,11 +473,11 @@ object Serializer {
}

private def s(node: Param)(implicit b: StringBuilder, indent: Int): Unit = node match {
case IntParam(name, value) => b ++= "parameter "; b ++= name; b ++= " = "; b ++= value.toString
case DoubleParam(name, value) => b ++= "parameter "; b ++= name; b ++= " = "; b ++= value.toString
case StringParam(name, value) => b ++= "parameter "; b ++= name; b ++= " = "; b ++= value.escape
case IntParam(name, value) => b ++= name; b ++= " = "; b ++= value.toString
case DoubleParam(name, value) => b ++= name; b ++= " = "; b ++= value.toString
case StringParam(name, value) => b ++= name; b ++= " = "; b ++= value.escape
case RawStringParam(name, value) =>
b ++= "parameter "; b ++= name; b ++= " = "
b ++= name; b ++= " = "
b += '\''; b ++= value.replace("'", "\\'"); b += '\''
case other => b ++= other.serialize // Handle user-defined nodes
}
Expand All @@ -467,14 +503,14 @@ object Serializer {
doIndent(0); b ++= "extmodule "; b ++= legalize(name); b ++= " :"; s(info)
ports.foreach { p => newLineAndIndent(1); s(p) }
newLineAndIndent(1); b ++= "defname = "; b ++= defname
params.foreach { p => newLineAndIndent(1); s(p) }
params.foreach { p => newLineAndIndent(1); b ++= "parameter "; s(p) }
Iterator(b.toString)
case IntModule(info, name, ports, intrinsic, params) =>
implicit val b = new StringBuilder
doIndent(0); b ++= "intmodule "; b ++= legalize(name); b ++= " :"; s(info)
ports.foreach { p => newLineAndIndent(1); s(p) }
newLineAndIndent(1); b ++= "intrinsic = "; b ++= intrinsic
params.foreach { p => newLineAndIndent(1); s(p) }
params.foreach { p => newLineAndIndent(1); b ++= "parameter "; s(p) }
Iterator(b.toString)
case DefClass(info, name, ports, body) =>
val start = {
Expand Down
16 changes: 16 additions & 0 deletions firrtl/src/test/scala/firrtlTests/SerializerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,22 @@ class SerializerSpec extends AnyFlatSpec with Matchers {
Serializer.serialize(probeRelease) should be("release(clock, cond, outProbe)")
}

it should "support emitting intrinsic expressions and statements" in {
val intrinsicNode =
DefNode(NoInfo, "foo", IntrinsicExpr("test", Seq(Reference("arg")), Seq(), UIntType(IntWidth(1))))
Serializer.serialize(intrinsicNode) should be("node foo = intrinsic(test : UInt<1>, arg)")

val paramsNode =
DefNode(NoInfo, "bar", IntrinsicExpr("test", Seq(), Seq(IntParam("foo", 5)), UIntType(IntWidth(1))))
Serializer.serialize(paramsNode) should be("node bar = intrinsic(test<foo = 5> : UInt<1>)")

val intrinsicStmt = IntrinsicStmt(NoInfo, "stmt", Seq(), Seq(IntParam("foo", 5)))
Serializer.serialize(intrinsicStmt) should be("intrinsic(stmt<foo = 5>)")

val intrinsicStmtTpe = IntrinsicStmt(NoInfo, "expr", Seq(), Seq(), Some(UIntType(IntWidth(2))))
Serializer.serialize(intrinsicStmtTpe) should be("intrinsic(expr : UInt<2>)")
}

it should "support lazy serialization" in {
var stmtSerialized = false
case class HackStmt(stmt: Statement) extends Statement {
Expand Down
77 changes: 77 additions & 0 deletions src/test/scala/chiselTests/IntrinsicSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: Apache-2.0

package chiselTests

import chiselTests.{ChiselFlatSpec, MatchesAndOmits}
import circt.stage.ChiselStage

import chisel3._

class IntrinsicSpec extends ChiselFlatSpec with MatchesAndOmits {
behavior.of("Intrinsics")

it should "support a simple intrinsic statement" in {
val chirrtl = ChiselStage.emitCHIRRTL(new RawModule {
Intrinsic("test")()
})

matchesAndOmits(chirrtl)("intrinsic(test)")()
}

it should "support intrinsic statements with arguments" in {
val chirrtl = ChiselStage.emitCHIRRTL(new RawModule {
val f = IO(UInt(3.W))
val g = IO(UInt(5.W))
Intrinsic("test")(f, g)
})

matchesAndOmits(chirrtl)("intrinsic(test, f, g)")()
}
it should "support intrinsic statements with parameters and arguments" in {
val chirrtl = ChiselStage.emitCHIRRTL(new RawModule {
val f = IO(UInt(3.W))
val g = IO(UInt(5.W))
Intrinsic("test", "Foo" -> 5)(f, g)
})

matchesAndOmits(chirrtl)("intrinsic(test<Foo = 5>, f, g)")()
}

it should "support intrinsic expressions" in {
val chirrtl = ChiselStage.emitCHIRRTL(new RawModule {
val f = IO(UInt(3.W))
val g = IO(UInt(5.W))
val test = IntrinsicExpr("test", UInt(32.W))(f, g) + 3.U
})

matchesAndOmits(chirrtl)(" = intrinsic(test : UInt<32>, f, g)")()
}

it should "support intrinsic expressions with parameters and arguments" in {
val chirrtl = ChiselStage.emitCHIRRTL(new RawModule {
val f = IO(UInt(3.W))
val g = IO(UInt(5.W))
val test = IntrinsicExpr("test", UInt(32.W), "foo" -> "bar", "x" -> 5)(f, g) + 3.U
})

matchesAndOmits(chirrtl)(" = intrinsic(test<foo = \"bar\", x = 5> : UInt<32>, f, g)")()
}

it should "preserve parameter order" in {
val chirrtl = ChiselStage.emitCHIRRTL(new RawModule {
val test = IntrinsicExpr("test", UInt(32.W), "x" -> 5, "foo" -> 3)()
})

matchesAndOmits(chirrtl)(" = intrinsic(test<x = 5, foo = 3> : UInt<32>)")()
}

it should "requite unique parameter names" in {
intercept[IllegalArgumentException] {
ChiselStage.emitCHIRRTL(new RawModule {
val test = IntrinsicExpr("test", UInt(32.W), "foo" -> 5, "foo" -> 3)()
})
}.getMessage() should include(
"parameter names must be unique"
)
}
}

0 comments on commit 87d4d45

Please sign in to comment.