From 4c90683f2843674815251e6670b594421ef43184 Mon Sep 17 00:00:00 2001 From: Noel Welsh Date: Wed, 6 Dec 2023 16:07:33 +0000 Subject: [PATCH] Initial import --- .github/workflows/ci.yml | 179 ++++++++++++++++++ .github/workflows/clean.yml | 59 ++++++ .gitignore | 145 ++++++++++++++ .scalafix.conf | 5 + .scalafmt.conf | 10 + README.md | 9 + .../scala/arithmetic/FibonacciBenchmark.scala | 76 ++++++++ build.sbt | 48 +++++ .../main/scala/arithmetic/BasicStack.scala | 89 +++++++++ core/src/main/scala/arithmetic/Compiled.scala | 5 + .../main/scala/arithmetic/Expression.scala | 13 ++ .../scala/arithmetic/OptimizedStack.scala | 93 +++++++++ .../scala/arithmetic/OptimizedStack2.scala | 100 ++++++++++ .../scala/arithmetic/OptimizedStack3.scala | 107 +++++++++++ .../arithmetic/StructuralRecursion.scala | 28 +++ .../scala/arithmetic/ArithmeticSuite.scala | 26 +++ .../scala/arithmetic/BasicStackSuite.scala | 3 + .../arithmetic/OptimizedStack2Suite.scala | 3 + .../arithmetic/OptimizedStackSuite.scala | 3 + .../arithmetic/StructuralRecursionSuite.scala | 4 + project/build.properties | 1 + project/plugins.sbt | 5 + 22 files changed, 1011 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/clean.yml create mode 100644 .gitignore create mode 100644 .scalafix.conf create mode 100644 .scalafmt.conf create mode 100644 README.md create mode 100644 benchmarks/src/main/scala/arithmetic/FibonacciBenchmark.scala create mode 100644 build.sbt create mode 100644 core/src/main/scala/arithmetic/BasicStack.scala create mode 100644 core/src/main/scala/arithmetic/Compiled.scala create mode 100644 core/src/main/scala/arithmetic/Expression.scala create mode 100644 core/src/main/scala/arithmetic/OptimizedStack.scala create mode 100644 core/src/main/scala/arithmetic/OptimizedStack2.scala create mode 100644 core/src/main/scala/arithmetic/OptimizedStack3.scala create mode 100644 core/src/main/scala/arithmetic/StructuralRecursion.scala create mode 100644 core/src/test/scala/arithmetic/ArithmeticSuite.scala create mode 100644 core/src/test/scala/arithmetic/BasicStackSuite.scala create mode 100644 core/src/test/scala/arithmetic/OptimizedStack2Suite.scala create mode 100644 core/src/test/scala/arithmetic/OptimizedStackSuite.scala create mode 100644 core/src/test/scala/arithmetic/StructuralRecursionSuite.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a950843 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,179 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Continuous Integration + +on: + pull_request: + branches: ['**', '!update/**', '!pr/**'] + push: + branches: ['**', '!update/**', '!pr/**'] + tags: [v*] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + +concurrency: + group: ${{ github.workflow }} @ ${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and Test + strategy: + matrix: + os: [ubuntu-latest] + scala: [3] + java: [temurin@8] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Check that workflows are up to date + run: sbt githubWorkflowCheck + + - name: Check headers and formatting + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + run: sbt '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck + + - name: Test + run: sbt '++ ${{ matrix.scala }}' test + + - name: Check binary compatibility + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + run: sbt '++ ${{ matrix.scala }}' mimaReportBinaryIssues + + - name: Generate API documentation + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + run: sbt '++ ${{ matrix.scala }}' doc + + - name: Make target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + run: mkdir -p target core/target benchmarks/target project/target + + - name: Compress target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + run: tar cf targets.tar target core/target benchmarks/target project/target + + - name: Upload target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + uses: actions/upload-artifact@v3 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }} + path: targets.tar + + publish: + name: Publish Artifacts + needs: [build] + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + strategy: + matrix: + os: [ubuntu-latest] + java: [temurin@8] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Download target directories (3) + uses: actions/download-artifact@v3 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3 + + - name: Inflate target directories (3) + run: | + tar xf targets.tar + rm targets.tar + + - name: Import signing key + if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: echo $PGP_SECRET | base64 -d -i - | gpg --import + + - name: Import signing key and strip passphrase + if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: | + echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg + echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg + (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) + + - name: Publish + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} + run: sbt tlCiRelease + + dependency-submission: + name: Submit Dependencies + if: github.event_name != 'pull_request' + strategy: + matrix: + os: [ubuntu-latest] + java: [temurin@8] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Submit Dependencies + uses: scalacenter/sbt-dependency-submission@v2 + with: + configs-ignore: test scala-tool scala-doc-tool test-internal diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml new file mode 100644 index 0000000..547aaa4 --- /dev/null +++ b/.github/workflows/clean.yml @@ -0,0 +1,59 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Clean + +on: push + +jobs: + delete-artifacts: + name: Delete Artifacts + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Delete artifacts + run: | + # Customize those three lines with your repository and credentials: + REPO=${GITHUB_API_URL}/repos/${{ github.repository }} + + # A shortcut to call GitHub API. + ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } + + # A temporary file which receives HTTP response headers. + TMPFILE=/tmp/tmp.$$ + + # An associative array, key: artifact name, value: number of artifacts of that name. + declare -A ARTCOUNT + + # Process all artifacts on this repository, loop on returned "pages". + URL=$REPO/actions/artifacts + while [[ -n "$URL" ]]; do + + # Get current page, get response headers in a temporary file. + JSON=$(ghapi --dump-header $TMPFILE "$URL") + + # Get URL of next page. Will be empty if we are at the last page. + URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') + rm -f $TMPFILE + + # Number of artifacts on this page: + COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) + + # Loop on all artifacts on this page. + for ((i=0; $i < $COUNT; i++)); do + + # Get name of artifact and count instances of this name. + name=$(jq <<<$JSON -r ".artifacts[$i].name?") + ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) + + id=$(jq <<<$JSON -r ".artifacts[$i].id?") + size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) + printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size + ghapi -X DELETE $REPO/actions/artifacts/$id + done + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3043b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,145 @@ +# Created by https://www.gitignore.io/api/scala,intellij,eclipse,sbt + +### Scala ### +*.class +*.log + +# sbt specific +.cache +.cache-main +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +project/target +project/project + +# Scala-IDE specific +.scala_dependencies +.worksheet + +# Documentation intermediate files +docs/src/main/paradox +docs/src/main/mdoc/api +docs/src/pages/**/*.png + + +### SublimeText ### +doodle.sublime-project +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties + + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + +# Metals and Bloop +.bsp +.metals +.bloop +project/metals.sbt + +.sbt-hydra-history diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 0000000..4eedd04 --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,5 @@ +rules = [ + OrganizeImports +] +OrganizeImports.removeUnused = false +OrganizeImports.coalesceToWildcardImportThreshold = 5 diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..7ca0d56 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,10 @@ +version = "3.0.0-RC6" +fileOverride { + "glob:**/core/shared/src/main/scala/**" { + runner.dialect = scala3 + } + "glob:**/core/shared/src/test/scala/**" { + runner.dialect = scala3 + } +} +runner.dialect = scala3 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a29c81 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Stack Machine + +A series of implementatinos of stack machines to optimize evaluation of arithmetic expressions. + + +## Benchmarks + +`benchmarks / Jmh / run` + diff --git a/benchmarks/src/main/scala/arithmetic/FibonacciBenchmark.scala b/benchmarks/src/main/scala/arithmetic/FibonacciBenchmark.scala new file mode 100644 index 0000000..a6d007e --- /dev/null +++ b/benchmarks/src/main/scala/arithmetic/FibonacciBenchmark.scala @@ -0,0 +1,76 @@ +/* + * Copyright 2023 Scala with Cats + * + * 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 arithmetic + +import org.openjdk.jmh.annotations._ + +import java.util.concurrent.TimeUnit.SECONDS + +@State(Scope.Benchmark) +@Warmup(iterations = 5, time = 1, timeUnit = SECONDS) +@Measurement(iterations = 1, time = 1, timeUnit = SECONDS) +class FibonnaciBenchmark { + def makeFibonacci[E <: Expression[E]]( + n: Int, + construct: ExpressionConstructors[E] + ): E = { + def loop(count: Int): E = + count match { + case 0 => construct.literal(0) + case 1 => construct.literal(1) + case n => loop(n - 1) + loop(n - 2) + } + + loop(n) + } + + val nFib = 25 + val expected = 75025 + + val baseFib = makeFibonacci(nFib, StructuralRecursion.Expression) + val basicStackFib = makeFibonacci(nFib, BasicStack.Expression).compile + val optimizedStackFib = makeFibonacci(nFib, OptimizedStack.Expression).compile + val optimizedStack2Fib = + makeFibonacci(nFib, OptimizedStack2.Expression).compile + val optimizedStack3Fib = + makeFibonacci(nFib, OptimizedStack3.Expression).compile + + @Benchmark + def baseFibBenchmark(): Unit = { + assert(baseFib.eval == expected) + } + + @Benchmark + def basicStackFibBenchmark(): Unit = { + assert(basicStackFib.eval == expected) + } + + @Benchmark + def optimizedStackFibBenchmark(): Unit = { + assert(optimizedStackFib.eval == expected) + } + + @Benchmark + def optimizedStack2FibBenchmark(): Unit = { + assert(optimizedStack2Fib.eval == expected) + } + + @Benchmark + def optimizedStack3FibBenchmark(): Unit = { + assert(optimizedStack3Fib.eval == expected) + } +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..0e74ca3 --- /dev/null +++ b/build.sbt @@ -0,0 +1,48 @@ +ThisBuild / scalaVersion := "3.3.1" + +ThisBuild / tlBaseVersion := "0.1" + +ThisBuild / organization := "com.scalawithcats" +ThisBuild / organizationName := "Scala with Cats" + +ThisBuild / semanticdbEnabled := true +ThisBuild / semanticdbVersion := scalafixSemanticdb.revision + +Global / onChangedBuildSource := ReloadOnSourceChanges + +val commonSettings = Seq( + libraryDependencies ++= Seq( + "org.typelevel" %% "cats-core" % "2.10.0", + "org.typelevel" %% "cats-effect" % "3.5.2", + "org.scalameta" %% "munit" % "0.7.29" % Test, + "org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % Test, + ) +) + +// Run this command (build) to do everything involved in building the project +commands += Command.command("build") { state => + "clean" :: + "compile" :: + "test" :: + "scalafixAll" :: + "scalafmtAll" :: + "headerCreateAll" :: + "githubWorkflowGenerate" :: + "dependencyUpdates" :: + "reload plugins; dependencyUpdates; reload return" :: + state +} +lazy val root = project.in(file(".")).aggregate(core, benchmarks) + +lazy val core = project.in(file("core")).settings(commonSettings) + +lazy val benchmarks = project + .in(file("benchmarks")) + .settings( + // javaOptions ++= Seq("-Xbatch", + // "-XX:+UnlockDiagnosticVMOptions", + // "-XX:CompileCommand=print,arithmetic/OptimizedStack2$StackMachine.eval", + // "-XX:CompileCommand=print,arithmetic/OptimizedStack$StackMachine.loop*") + ) + .enablePlugins(JmhPlugin) + .dependsOn(core) diff --git a/core/src/main/scala/arithmetic/BasicStack.scala b/core/src/main/scala/arithmetic/BasicStack.scala new file mode 100644 index 0000000..a98dfc4 --- /dev/null +++ b/core/src/main/scala/arithmetic/BasicStack.scala @@ -0,0 +1,89 @@ +package arithmetic + +object BasicStack { + + enum Expression extends arithmetic.Expression[Expression] { + case Literal(value: Double) + case Addition(left: Expression, right: Expression) + case Subtraction(left: Expression, right: Expression) + case Multiplication(left: Expression, right: Expression) + case Division(left: Expression, right: Expression) + + def +(that: Expression): Expression = Addition(this, that) + def *(that: Expression): Expression = Multiplication(this, that) + def -(that: Expression): Expression = Subtraction(this, that) + def /(that: Expression): Expression = Division(this, that) + + def compile: Program = { + def loop(expr: Expression): List[Op] = + expr match { + case Literal(value) => List(Op.Lit(value)) + case Addition(left, right) => + loop(left) ++ loop(right) ++ List(Op.Add) + case Subtraction(left, right) => + loop(left) ++ loop(right) ++ List(Op.Sub) + case Multiplication(left, right) => + loop(left) ++ loop(right) ++ List(Op.Mul) + case Division(left, right) => + loop(left) ++ loop(right) ++ List(Op.Div) + } + + Program(loop(this)) + } + + def eval: Double = compile.eval + } + object Expression extends arithmetic.ExpressionConstructors[Expression] { + def literal(value: Double): Expression = Literal(value) + } + + enum Op { + case Lit(value: Double) + case Add + case Sub + case Mul + case Div + } + + final case class Program(program: List[Op]) { + val machine = new StackMachine(program) + + def eval: Double = machine.eval + } + + class StackMachine(val program: List[Op]) { + def eval: Double = { + def pop2(data: List[Double]): (Double, Double, List[Double]) = + data match { + case a :: b :: next => (a, b, next) + case other => + throw new IllegalStateException( + s"The data stack does not have the required two elements. Stack: $data" + ) + } + def loop(data: List[Double], program: List[Op]): Double = + program match { + case head :: next => + head match { + case Op.Lit(value) => loop(value :: data, next) + case Op.Add => + val (a, b, rest) = pop2(data) + loop((a + b) :: rest, next) + case Op.Sub => + val (a, b, rest) = pop2(data) + loop((a - b) :: rest, next) + case Op.Mul => + val (a, b, rest) = pop2(data) + loop((a * b) :: rest, next) + case Op.Div => + val (a, b, rest) = pop2(data) + loop((a / b) :: rest, next) + } + + case Nil => data.head + } + + loop(List.empty, program) + } + } +} diff --git a/core/src/main/scala/arithmetic/Compiled.scala b/core/src/main/scala/arithmetic/Compiled.scala new file mode 100644 index 0000000..ccb4b33 --- /dev/null +++ b/core/src/main/scala/arithmetic/Compiled.scala @@ -0,0 +1,5 @@ +package arithmetic + +trait Compiled[P] { + def compile: P +} diff --git a/core/src/main/scala/arithmetic/Expression.scala b/core/src/main/scala/arithmetic/Expression.scala new file mode 100644 index 0000000..d5237bd --- /dev/null +++ b/core/src/main/scala/arithmetic/Expression.scala @@ -0,0 +1,13 @@ +package arithmetic + +trait Expression[E <: Expression[E]] { + def +(that: E): E + def *(that: E): E + def -(that: E): E + def /(that: E): E + + def eval: Double +} +trait ExpressionConstructors[E <: Expression[E]] { + def literal(value: Double): E +} diff --git a/core/src/main/scala/arithmetic/OptimizedStack.scala b/core/src/main/scala/arithmetic/OptimizedStack.scala new file mode 100644 index 0000000..e22eeb6 --- /dev/null +++ b/core/src/main/scala/arithmetic/OptimizedStack.scala @@ -0,0 +1,93 @@ +package arithmetic + +object OptimizedStack { + + enum Expression extends arithmetic.Expression[Expression] { + case Literal(value: Double) + case Addition(left: Expression, right: Expression) + case Subtraction(left: Expression, right: Expression) + case Multiplication(left: Expression, right: Expression) + case Division(left: Expression, right: Expression) + + def +(that: Expression): Expression = Addition(this, that) + def *(that: Expression): Expression = Multiplication(this, that) + def -(that: Expression): Expression = Subtraction(this, that) + def /(that: Expression): Expression = Division(this, that) + + def compile: Program = { + def loop(expr: Expression): List[Op] = + expr match { + case Literal(value) => List(Op.Lit(value)) + case Addition(left, right) => + loop(left) ++ loop(right) ++ List(Op.Add) + case Subtraction(left, right) => + loop(left) ++ loop(right) ++ List(Op.Sub) + case Multiplication(left, right) => + loop(left) ++ loop(right) ++ List(Op.Mul) + case Division(left, right) => + loop(left) ++ loop(right) ++ List(Op.Div) + } + + Program(loop(this)) + } + + def eval: Double = compile.eval + } + object Expression extends arithmetic.ExpressionConstructors[Expression] { + def literal(value: Double): Expression = Literal(value) + } + + enum Op { + case Lit(value: Double) + case Add + case Sub + case Mul + case Div + } + + final case class Program(program: List[Op]) { + val machine = new StackMachine(program.toArray) + + def eval: Double = machine.eval + } + + class StackMachine(program: Array[Op]) { + // The data stack + private val data: Array[Double] = Array.ofDim[Double](256) + + final def eval: Double = { + // sp points to first free element on the stack + // data(sp - 1) is the first element + def loop(sp: Int, pc: Int): Double = + if (pc == program.size) data(sp - 1) + else + program(pc) match { + case Op.Lit(value) => + data(sp) = value + loop(sp + 1, pc + 1) + case Op.Add => + val a = data(sp - 1) + val b = data(sp - 2) + data(sp - 2) = (a + b) + loop(sp - 1, pc + 1) + case Op.Sub => + val a = data(sp - 1) + val b = data(sp - 2) + data(sp - 2) = (a - b) + loop(sp - 1, pc + 1) + case Op.Mul => + val a = data(sp - 1) + val b = data(sp - 2) + data(sp - 2) = (a * b) + loop(sp - 1, pc + 1) + case Op.Div => + val a = data(sp - 1) + val b = data(sp - 2) + data(sp - 2) = (a / b) + loop(sp - 1, pc + 1) + } + + loop(0, 0) + } + } +} diff --git a/core/src/main/scala/arithmetic/OptimizedStack2.scala b/core/src/main/scala/arithmetic/OptimizedStack2.scala new file mode 100644 index 0000000..b1c2aa4 --- /dev/null +++ b/core/src/main/scala/arithmetic/OptimizedStack2.scala @@ -0,0 +1,100 @@ +package arithmetic + +object OptimizedStack2 { + + enum Expression extends arithmetic.Expression[Expression] { + case Literal(value: Double) + case Addition(left: Expression, right: Expression) + case Subtraction(left: Expression, right: Expression) + case Multiplication(left: Expression, right: Expression) + case Division(left: Expression, right: Expression) + + def +(that: Expression): Expression = Addition(this, that) + def *(that: Expression): Expression = Multiplication(this, that) + def -(that: Expression): Expression = Subtraction(this, that) + def /(that: Expression): Expression = Division(this, that) + + def compile: Program = { + def loop(expr: Expression): List[Op] = + expr match { + case Literal(value) => List(Op.Lit(value)) + case Addition(left, right) => + loop(left) ++ loop(right) ++ List(Op.Add) + case Subtraction(left, right) => + loop(left) ++ loop(right) ++ List(Op.Sub) + case Multiplication(left, right) => + loop(left) ++ loop(right) ++ List(Op.Mul) + case Division(left, right) => + loop(left) ++ loop(right) ++ List(Op.Div) + } + + Program(loop(this)) + } + + def eval: Double = compile.eval + } + object Expression extends arithmetic.ExpressionConstructors[Expression] { + def literal(value: Double): Expression = Literal(value) + } + + enum Op { + case Lit(value: Double) + case Add + case Sub + case Mul + case Div + } + + final case class Program(program: List[Op]) { + val machine = new StackMachine(program.toArray) + + def eval: Double = machine.eval + } + + class StackMachine(program: Array[Op]) { + // The data stack + private val data: Array[Double] = Array.ofDim[Double](256) + + final def eval: Double = { + // sp points to first free element on the stack + // data(sp - 1) is the first element + var sp: Int = 0 + var pc: Int = 0 + + val size = program.size + while (pc < size) { + program(pc) match { + case Op.Lit(value) => + data(sp) = value + sp = sp + 1 + pc = pc + 1 + case Op.Add => + val a = data(sp - 1) + val b = data(sp - 2) + data(sp - 2) = (a + b) + sp = sp - 1 + pc = pc + 1 + case Op.Sub => + val a = data(sp - 1) + val b = data(sp - 2) + data(sp - 2) = (a - b) + sp = sp - 1 + pc = pc + 1 + case Op.Mul => + val a = data(sp - 1) + val b = data(sp - 2) + data(sp - 2) = (a * b) + sp = sp - 1 + pc = pc + 1 + case Op.Div => + val a = data(sp - 1) + val b = data(sp - 2) + data(sp - 2) = (a / b) + sp = sp - 1 + pc = pc + 1 + } + } + data(sp - 1) + } + } +} diff --git a/core/src/main/scala/arithmetic/OptimizedStack3.scala b/core/src/main/scala/arithmetic/OptimizedStack3.scala new file mode 100644 index 0000000..acac8a1 --- /dev/null +++ b/core/src/main/scala/arithmetic/OptimizedStack3.scala @@ -0,0 +1,107 @@ +package arithmetic + +object OptimizedStack3 { + + enum Expression extends arithmetic.Expression[Expression] { + case Literal(value: Double) + case Addition(left: Expression, right: Expression) + case Subtraction(left: Expression, right: Expression) + case Multiplication(left: Expression, right: Expression) + case Division(left: Expression, right: Expression) + + def +(that: Expression): Expression = Addition(this, that) + def *(that: Expression): Expression = Multiplication(this, that) + def -(that: Expression): Expression = Subtraction(this, that) + def /(that: Expression): Expression = Division(this, that) + + def compile: Program = { + def loop(expr: Expression): List[Op] = + expr match { + case Literal(value) => List(Op.Lit(value)) + case Addition(left, right) => + loop(left) ++ loop(right) ++ List(Op.Add) + case Subtraction(left, right) => + loop(left) ++ loop(right) ++ List(Op.Sub) + case Multiplication(left, right) => + loop(left) ++ loop(right) ++ List(Op.Mul) + case Division(left, right) => + loop(left) ++ loop(right) ++ List(Op.Div) + } + + Program(loop(this)) + } + + def eval: Double = compile.eval + } + object Expression extends arithmetic.ExpressionConstructors[Expression] { + def literal(value: Double): Expression = Literal(value) + } + + enum Op { + case Lit(value: Double) + case Add + case Sub + case Mul + case Div + } + + final case class Program(program: List[Op]) { + val machine = new StackMachine(program.toArray) + + def eval: Double = machine.eval + } + + class StackMachine(program: Array[Op]) { + // The data stack + private val data: Array[Double] = Array.ofDim[Double](256) + + final def eval: Double = { + // sp points to first free element on the stack + // data(sp - 1) is the first element + var sp: Int = 0 + var pc: Int = 0 + + def push(value: Double): Unit = { + data(sp) = value + sp = sp + 1 + } + + def pop(): Double = { + val result = data(sp - 1) + sp = sp - 1 + result + } + + val size = program.size + while (pc < size) { + program(pc) match { + case Op.Lit(value) => + push(value) + pc = pc + 1 + case Op.Add => + val a = pop() + val b = pop() + push(a + b) + pc = pc + 1 + case Op.Sub => + val a = pop() + val b = pop() + push(a - b) + pc = pc + 1 + case Op.Mul => + val a = pop() + val b = pop() + push(a * b) + pc = pc + 1 + case Op.Div => + val a = pop() + val b = pop() + push(a / b) + sp = sp - 1 + pc = pc + 1 + } + } + data(sp - 1) + } + } +} diff --git a/core/src/main/scala/arithmetic/StructuralRecursion.scala b/core/src/main/scala/arithmetic/StructuralRecursion.scala new file mode 100644 index 0000000..649412c --- /dev/null +++ b/core/src/main/scala/arithmetic/StructuralRecursion.scala @@ -0,0 +1,28 @@ +package arithmetic + +object StructuralRecursion { + enum Expression extends arithmetic.Expression[Expression] { + case Literal(value: Double) + case Addition(left: Expression, right: Expression) + case Subtraction(left: Expression, right: Expression) + case Multiplication(left: Expression, right: Expression) + case Division(left: Expression, right: Expression) + + def +(that: Expression): Expression = Addition(this, that) + def *(that: Expression): Expression = Multiplication(this, that) + def -(that: Expression): Expression = Subtraction(this, that) + def /(that: Expression): Expression = Division(this, that) + + def eval: Double = + this match { + case Literal(value) => value + case Addition(left, right) => left.eval + right.eval + case Subtraction(left, right) => left.eval - right.eval + case Multiplication(left, right) => left.eval * right.eval + case Division(left, right) => left.eval / right.eval + } + } + object Expression extends arithmetic.ExpressionConstructors[Expression] { + def literal(value: Double): Expression = Literal(value) + } +} diff --git a/core/src/test/scala/arithmetic/ArithmeticSuite.scala b/core/src/test/scala/arithmetic/ArithmeticSuite.scala new file mode 100644 index 0000000..156b59e --- /dev/null +++ b/core/src/test/scala/arithmetic/ArithmeticSuite.scala @@ -0,0 +1,26 @@ +package arithmetic + +import munit.FunSuite + +trait ArithmeticSuite[E <: Expression[E]](construct: ExpressionConstructors[E]) + extends FunSuite { + def makeFibonacci(n: Int): E = { + def loop(count: Int): E = + count match { + case 0 => construct.literal(0) + case 1 => construct.literal(1) + case n => loop(n - 1) + loop(n - 2) + } + + loop(n) + } + + test("Fibonacci numbers are calculated correctly") { + val n = List.range(1, 16) + assertEquals( + n.map(n => makeFibonacci(n).eval), + List(1.0, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 21.0, 34.0, 55.0, 89.0, 144.0, + 233.0, 377.0, 610.0) + ) + } +} diff --git a/core/src/test/scala/arithmetic/BasicStackSuite.scala b/core/src/test/scala/arithmetic/BasicStackSuite.scala new file mode 100644 index 0000000..243949a --- /dev/null +++ b/core/src/test/scala/arithmetic/BasicStackSuite.scala @@ -0,0 +1,3 @@ +package arithmetic + +class BasicStackSuite extends ArithmeticSuite(BasicStack.Expression) diff --git a/core/src/test/scala/arithmetic/OptimizedStack2Suite.scala b/core/src/test/scala/arithmetic/OptimizedStack2Suite.scala new file mode 100644 index 0000000..76dbb5e --- /dev/null +++ b/core/src/test/scala/arithmetic/OptimizedStack2Suite.scala @@ -0,0 +1,3 @@ +package arithmetic + +class OptimizedStack2Suite extends ArithmeticSuite(OptimizedStack2.Expression) diff --git a/core/src/test/scala/arithmetic/OptimizedStackSuite.scala b/core/src/test/scala/arithmetic/OptimizedStackSuite.scala new file mode 100644 index 0000000..6b187e5 --- /dev/null +++ b/core/src/test/scala/arithmetic/OptimizedStackSuite.scala @@ -0,0 +1,3 @@ +package arithmetic + +class OptimizedStackSuite extends ArithmeticSuite(OptimizedStack.Expression) diff --git a/core/src/test/scala/arithmetic/StructuralRecursionSuite.scala b/core/src/test/scala/arithmetic/StructuralRecursionSuite.scala new file mode 100644 index 0000000..fae84d2 --- /dev/null +++ b/core/src/test/scala/arithmetic/StructuralRecursionSuite.scala @@ -0,0 +1,4 @@ +package arithmetic + +class StructuralRecursionSuite + extends ArithmeticSuite(StructuralRecursion.Expression) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..e8a1e24 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.7 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..480bae0 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,5 @@ +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.3") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.6.3") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6")