diff --git a/hammer_ir/scalalib/.gitignore b/hammer_ir/scalalib/.gitignore new file mode 100644 index 000000000..b6ae9c889 --- /dev/null +++ b/hammer_ir/scalalib/.gitignore @@ -0,0 +1,5 @@ +out/ +*.jar +amm +mill +test/tmp_* diff --git a/hammer_ir/scalalib/README.md b/hammer_ir/scalalib/README.md new file mode 100644 index 000000000..614f62824 --- /dev/null +++ b/hammer_ir/scalalib/README.md @@ -0,0 +1,9 @@ +Hammer IR Scala library +======================= + +This API is experimental and subject to change at any time. + +To get a JAR, run `./build.sh`. +You can find the output JAR in `out/hammer_ir/assembly/dest/out.jar`. + +TODO(edwardw): write documentation for what this is and how to use it. diff --git a/hammer_ir/scalalib/build.sc b/hammer_ir/scalalib/build.sc new file mode 100644 index 000000000..f40b49ea9 --- /dev/null +++ b/hammer_ir/scalalib/build.sc @@ -0,0 +1,17 @@ +import mill._, scalalib._ + +object hammer_ir extends SbtModule { + def scalaVersion = "2.12.6" + + // Unrooted submodule + override def millSourcePath = super.millSourcePath / ammonite.ops.up + + def ivyDeps = Agg( + ivy"com.typesafe.play::play-json:2.6.10" + ) + + object test extends Tests { + def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.4") + def testFrameworks = Seq("org.scalatest.tools.Framework") + } +} diff --git a/hammer_ir/scalalib/build.sh b/hammer_ir/scalalib/build.sh new file mode 100755 index 000000000..e856c02c4 --- /dev/null +++ b/hammer_ir/scalalib/build.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [ ! -f "./mill" ]; then + ./get_mill.sh +fi +./mill hammer_ir.assembly +cp out/hammer_ir/assembly/dest/out.jar hammer_ir.jar diff --git a/hammer_ir/scalalib/get_ammonite.sh b/hammer_ir/scalalib/get_ammonite.sh new file mode 100755 index 000000000..97186aeee --- /dev/null +++ b/hammer_ir/scalalib/get_ammonite.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -ex +wget -nv https://github.com/lihaoyi/Ammonite/releases/download/1.6.6/2.12-1.6.6 -O amm +chmod +x ./amm diff --git a/hammer_ir/scalalib/get_mill.sh b/hammer_ir/scalalib/get_mill.sh new file mode 100755 index 000000000..7b378225f --- /dev/null +++ b/hammer_ir/scalalib/get_mill.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -ex +wget -nv https://github.com/lihaoyi/mill/releases/download/0.3.6/0.3.6 -O mill +chmod +x ./mill diff --git a/hammer_ir/scalalib/scalatest.sh b/hammer_ir/scalalib/scalatest.sh new file mode 100755 index 000000000..f9833a348 --- /dev/null +++ b/hammer_ir/scalalib/scalatest.sh @@ -0,0 +1,2 @@ +#!/bin/sh +./mill hammer_ir.test diff --git a/hammer_ir/scalalib/src/main/scala/HammerObject.scala b/hammer_ir/scalalib/src/main/scala/HammerObject.scala new file mode 100644 index 000000000..25bf1cc6f --- /dev/null +++ b/hammer_ir/scalalib/src/main/scala/HammerObject.scala @@ -0,0 +1,62 @@ +// See LICENSE for licence details. + +package hammer_ir + +import scala.reflect.ClassTag + +import play.api.libs.json.{Json, JsObject, JsValue} + +/** + * All Hammer IR types that can be converted to a JsValue. + */ +trait JSONConvertible { + /** + * Turn this object into a Play JSON object. + */ + private[hammer_ir] def toJSON: JsValue +} + +/** + * All Hammer IR objects implement this trait which allows them to be + * serialized to/deserialized from JSON. + */ +trait HammerObject { + /** + * Turn this object into a Play JSON object. + */ + private[hammer_ir] def toJSON: JsObject + + /** + * Turn this object into a JSON string. + */ + override def toString = Json.prettyPrint(toJSON) + + /** + * Write this object as a JSON string into the given file. + */ + def toFile(filename: String) = reflect.io.File(filename).writeAll(toString) +} + +/** + * Abstract class mixed into companion classes of Hammer IR objects. + */ +abstract class HammerObjectCompanion[T <: HammerObject : ClassTag] { + /** + * Create this object from a Play JSON object. + */ + private[hammer_ir] def fromJSON(json: JsObject): T + + /** + * Create this object from a JSON string. + */ + def fromString(json: String): T = fromJSON(Json.parse(json).as[JsObject]) + + /** + * Create this object from a JSON file. + */ + def fromFile(filename: String): T = { + val source = scala.io.Source.fromFile(filename) + val lines = try source.mkString finally source.close() + fromString(lines) + } +} diff --git a/hammer_ir/scalalib/src/main/scala/PlacementConstraint.scala b/hammer_ir/scalalib/src/main/scala/PlacementConstraint.scala new file mode 100644 index 000000000..77beefba2 --- /dev/null +++ b/hammer_ir/scalalib/src/main/scala/PlacementConstraint.scala @@ -0,0 +1,136 @@ +// See LICENSE for licence details. + +package hammer_ir + +import play.api.libs.json.{JsNumber, JsObject, JsString} + +sealed abstract class PlacementConstraintType extends JSONConvertible { + override def toJSON = JsString(stringVal) + // Force objects to implement this since toString is defined by + // default. + def stringVal: String + override def toString = stringVal +} +object PlacementConstraintType { + def fromString(input: String): PlacementConstraintType = { + values.foreach { i => + if (i.stringVal == input) return i + } + throw new IllegalArgumentException(s"Illegal PlacementConstraintType $input") + } + + case object Dummy extends PlacementConstraintType { + override def stringVal = "dummy" + } + case object Placement extends PlacementConstraintType { + override def stringVal = "placement" + } + case object TopLevel extends PlacementConstraintType { + override def stringVal = "toplevel" + } + case object HardMacro extends PlacementConstraintType { + override def stringVal = "hardmacro" + } + case object Hierarchical extends PlacementConstraintType { + override def stringVal = "hierarchical" + } + case object Obstruction extends PlacementConstraintType { + override def stringVal = "obstruction" + } + + def values = Seq( + Dummy, Placement, TopLevel, HardMacro, Hierarchical, Obstruction + ) +} + +sealed abstract class ObstructionType +object ObstructionType { + case object Place extends ObstructionType + case object Route extends ObstructionType + case object Power extends ObstructionType +} + +case class Margins( + left: Double, + bottom: Double, + right: Double, + top: Double +) + +case class PlacementConstraint( + path: String, + `type`: PlacementConstraintType, + x: Double, + y: Double, + width: Double, + height: Double, + orientation: Option[String], + margins: Option[Margins], + top_layer: Option[String], + layers: Option[Seq[String]], + obs_types: Option[Seq[ObstructionType]] +) extends HammerObject { + override def toJSON = { + JsObject(Seq( + "path" -> JsString(path), + "type" -> `type`.toJSON, + "x" -> JsNumber(x), + "y" -> JsNumber(y), + "width" -> JsNumber(width), + "height" -> JsNumber(height) + // TODO(edwardw): FIXME + )) + } +} + +object PlacementConstraint extends HammerObjectCompanion[PlacementConstraint] { + def apply( + path: String, + `type`: PlacementConstraintType, + x: Double, + y: Double, + width: Double, + height: Double + ): PlacementConstraint = { + new PlacementConstraint( + path, + `type`, + x, + y, + width, + height, + None, // TODO(edwardw): FIXME + None, + None, + None, + None + ) + } + + /* Helper function used to convert strings to Double + * for forwards-compatibility with Decimal type. */ + def doubleOrString(value: play.api.libs.json.JsLookupResult): Double = { + try { + value.as[Double] + } catch { + case _: play.api.libs.json.JsResultException => + value.as[String].toDouble + } + } + + override def fromJSON(json: JsObject): PlacementConstraint = { + new PlacementConstraint( + (json \ "path").as[String], + PlacementConstraintType.fromString((json \ "type").as[String]), + doubleOrString(json \ "x"), + doubleOrString(json \ "y"), + doubleOrString(json \ "width"), + doubleOrString(json \ "height"), + None, // TODO(edwardw): FIXME + None, + None, + None, + None + ) + } +} diff --git a/hammer_ir/scalalib/src/test/scala/HammerObjectSpec.scala b/hammer_ir/scalalib/src/test/scala/HammerObjectSpec.scala new file mode 100644 index 000000000..13562bbb6 --- /dev/null +++ b/hammer_ir/scalalib/src/test/scala/HammerObjectSpec.scala @@ -0,0 +1,34 @@ +// See LICENSE for licence details. + +package hammer_ir.test + +import org.scalatest.{FlatSpec, Matchers} +import play.api.libs.json.{JsObject, JsString} + +import hammer_ir._ + +class HammerObjectSpec extends FlatSpec with Matchers { + behavior of "HammerObject" + + case class Test(foobar: String) extends HammerObject { + override def toJSON = JsObject(Seq( + "foobar" -> JsString(foobar) + )) + } + object Test extends HammerObjectCompanion[Test] { + override def fromJSON(json: JsObject): Test = { + new Test( + (json \ "foobar").as[String] + ) + } + } + + it should "serialize and deserialize correctly" in { + val t = Test("helloworld") + // Need these replaces to be robust to spacing variations + assert(t.toString + .replaceAll("\n", "").replaceAll(" ", "") + === """{"foobar":"helloworld"}""") + assert(Test.fromJSON(t.toJSON) == t) + } +} diff --git a/hammer_ir/scalalib/test/placement_test/script.scala b/hammer_ir/scalalib/test/placement_test/script.scala new file mode 100644 index 000000000..506f8060b --- /dev/null +++ b/hammer_ir/scalalib/test/placement_test/script.scala @@ -0,0 +1,15 @@ +// See LICENSE for licence details. + +import $cp.`hammer_ir.jar` + +import hammer_ir._ + +// Read and write back +val p = PlacementConstraint.fromFile("c1.json") + +p.`type` match { + case PlacementConstraintType.Placement => println("Am placement") + case _ => println("Am not placement") +} + +p.toFile("c1-out.json") diff --git a/hammer_ir/scalalib/test/prep.sh b/hammer_ir/scalalib/test/prep.sh new file mode 100755 index 000000000..9700ffef4 --- /dev/null +++ b/hammer_ir/scalalib/test/prep.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Test preparation script. +# Ensures that ammonite and the Hammer IR JAR are built. + +set -e +set -euo pipefail + +script_dir=$(dirname $0) +cd $script_dir + +# Build Hammer IR +pushd .. +./build.sh + +# Ensure ammonite exists +if [ ! -f "amm" ]; then + ./get_ammonite.sh +fi + +popd + +# Create ammonite wrapper to import hammer_ir JAR. +echo "../amm --predef-code 'import ammonite.ops._; interp.load.cp(pwd/os.up/\"hammer_ir.jar\")' \$1" > amm +chmod +x amm diff --git a/hammer_ir/scalalib/test/run_tests.sh b/hammer_ir/scalalib/test/run_tests.sh new file mode 100755 index 000000000..efc83447d --- /dev/null +++ b/hammer_ir/scalalib/test/run_tests.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -ex +cd $(dirname $0) +./test_* diff --git a/hammer_ir/scalalib/test/test_placement.py b/hammer_ir/scalalib/test/test_placement.py new file mode 100755 index 000000000..f37fcfbf1 --- /dev/null +++ b/hammer_ir/scalalib/test/test_placement.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Test placement constraints in Hammer IR. +# +# See LICENSE for licence details. + +import json +import os +import sys + +# TODO(edwardw): separate hammer_ir out of hammer_vlsi +from hammer_vlsi import PlacementConstraint, PlacementConstraintType + +# TODO(edwardw): move these to a separate file/library +prepped = False # type: bool +def prep() -> None: + assert os.system("./prep.sh") == 0 + global prepped + prepped = True + +def run_scala(scala: str) -> None: + assert prepped, "Must prep before running test functions" + + with open("tmp_script.scala", "w") as f: + f.write(scala) + + if os.system("./amm tmp_script.scala") != 0: + print("Scala script below failed: \n{}".format(scala), file=sys.stderr) + sys.exit(1) + +prep() + +# Generate some constraints +c1 = PlacementConstraint( + path="Top/rtl/a/b", + type=PlacementConstraintType.Placement, + x=15.123, + y=14.567, + width=42.900, + height=888.100, + orientation=None, + margins=None, + top_layer=None, + layers=None, + obs_types=None +) + +# TODO(edwardw): the optional parameters are only valid for certain +# types of constraints. +# e.g. orientation is only valid for hardmacros +# e.g. obs_types is only valid for obstructions +# We need to create some extra testcases to capture those usecases. + +# Export to JSON +with open("tmp_c1.json", "w") as f: + f.write(json.dumps(c1.to_dict())) + +# Check that it's still usable in Scala-land +run_scala(""" +import hammer_ir._ + +// Read and write back +val p = PlacementConstraint.fromFile("tmp_c1.json") + +assert(p.path == "Top/rtl/a/b") +assert(p.`type` == PlacementConstraintType.Placement) +assert(p.x == 15.123) +assert(p.y == 14.567) +assert(p.width == 42.900) +assert(p.height == 888.100) + +p.toFile("tmp_c1-out.json") +""") + +# Check that it's still the same +with open("tmp_c1-out.json", "r") as f: + c1_out = PlacementConstraint.from_dict(json.loads(f.read())) + +print(c1_out) +assert c1 == c1_out diff --git a/src/test/mypy.sh b/src/test/mypy.sh index 4c6d166d9..c4b3345ba 100755 --- a/src/test/mypy.sh +++ b/src/test/mypy.sh @@ -37,6 +37,9 @@ call_mypy ../hammer-vlsi/par/mockpar/__init__.py call_mypy ../hammer-vlsi/drc/*.py call_mypy ../hammer-vlsi/lvs/*.py +# Scala library +call_mypy ../../hammer_ir/scalalib/test/*.py + # Plugins which may or may not exist if [ -f ../hammer-vlsi/synthesis/dc/__init__.py ]; then call_mypy ../hammer-vlsi/synthesis/dc/__init__.py