Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • shchen/cs320
  • raveendr/cs320
  • mwojnaro/cs320
3 results
Show changes
Showing
with 515 additions and 0 deletions
File added
File added
File added
File added
File added
File added
version := "1.7"
organization := "ch.epfl.lara"
scalaVersion := "3.5.2"
assembly / test := {}
name := "amyc"
Compile / scalaSource := baseDirectory.value / "src"
scalacOptions ++= Seq("-feature")
Test / scalaSource := baseDirectory.value / "test" / "scala"
Test / parallelExecution := false
libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test"
libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.4" % "test"
testOptions += Tests.Argument(TestFrameworks.JUnit, "-v")
assembly / assemblyMergeStrategy :=
{
{
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case _ => MergeStrategy.first
}
}
object Arithmetic
def pow(b: Int(32), e: Int(32)): Int(32) = {
if (e == 0) { 1 }
else {
if (e % 2 == 0) {
val rec: Int(32) = pow(b, e/2);
rec * rec
} else {
b * pow(b, e - 1)
}
}
}
def gcd(a: Int(32), b: Int(32)): Int(32) = {
if (a == 0 || b == 0) {
a + b
} else {
if (a < b) {
gcd(a, b % a)
} else {
gcd(a % b, b)
}
}
}
Std.printInt(pow(0, 10));
Std.printInt(pow(1, 5));
Std.printInt(pow(2, 10));
Std.printInt(pow(3, 3));
Std.printInt(gcd(0, 10));
Std.printInt(gcd(17, 99)); // 1
Std.printInt(gcd(16, 46)); // 2
Std.printInt(gcd(222, 888)) // 222
end Arithmetic
object Factorial
def fact(i: Int(32)): Int(32) = {
if (i < 2) { 1 }
else {
val rec: Int(32) = fact(i-1);
i * rec
}
}
Std.printString("5! = " ++ Std.intToString(fact(5)));
Std.printString("10! = " ++ Std.intToString(fact(10)))
end Factorial
object Hanoi
def solve(n : Int(32)) : Int(32) = {
if (n < 1) {
error("can't solve Hanoi for less than 1 plate")
} else {
if (n == 1) {
1
} else {
2 * solve(n - 1) + 1
}
}
}
Std.printString("Hanoi for 4 plates: " ++ Std.intToString(solve(4)))
end Hanoi
\ No newline at end of file
object Hello
Std.printString("Hello " ++ "world!")
end Hello
object HelloInt
Std.printString("What is your name?");
val name: String = Std.readString();
Std.printString("Hello " ++ name ++ "! And how old are you?");
val age: Int(32) = Std.readInt();
Std.printString(Std.intToString(age) ++ " years old then.")
end HelloInt
object Printing
Std.printInt(0); Std.printInt(-222); Std.printInt(42);
Std.printBoolean(true); Std.printBoolean(false);
Std.printString(Std.digitToString(0));
Std.printString(Std.digitToString(5));
Std.printString(Std.digitToString(9));
Std.printString(Std.intToString(0));
Std.printString(Std.intToString(-111));
Std.printString(Std.intToString(22));
Std.printString("Hello " ++ "world!");
Std.printString("" ++ "")
end Printing
object TestLists
val l: L.List = L.Cons(5, L.Cons(-5, L.Cons(-1, L.Cons(0, L.Cons(10, L.Nil())))));
Std.printString(L.toString(L.concat(L.Cons(1, L.Cons(2, L.Nil())), L.Cons(3, L.Nil()))));
Std.printInt(L.sum(l));
Std.printString(L.toString(L.mergeSort(l)))
end TestLists
# Lab 05: Code Generation
## Introduction
Welcome to the last common assignment for the Amy compiler. At this point, we are finally done with the frontend: we have translated source programs to ASTs and have checked that certain correctness conditions hold for our program. We are ready to generate code for our program. In our case the target language will be _WebAssembly_.
WebAssembly is "a new portable, size- and load-time-efficient format suitable for compilation to the web" (<http://webassembly.org>). WebAssembly was initially designed to be called from JavaScript in browsers and lends itself to highly-performant execution. Nowadays, WebAssembly is getting some traction in many different contexts, including server-side applications and embedded systems.
For simplicity, we will not use a browser, but execute the resulting WebAssembly bytecode directly using `wasmtime` which is WebAssembly virtual machine implementation. When you run your complete compiler (or the reference compiler) with no options on program `p`, it will generate two different files under the `wasmout` directory:
- `p.wat` is the wasm output of the compiler in a human readable text format. You can use this representation to debug your generated code.
- `p.wasm` is the binary output of the compiler. This is what `wasmtime` will read. To translate to the binary format, we use the `wat2wasm` tool provided by the WebAssembly developers. Note that this tool performs a purely mechanical translation and thus its output (for instance, `p.wasm`) corresponds to a binary representation of `p.wat`.
To run the program, simply type `wasmtime wasmout/p.wasm`
### Installing `wat2wasm` and `wasmtime`
Both `wat2wasm` and `wasmtime` are bundled for you under `/<root of the project>/bin/<platform>`. You may however install them yourself and place them either in your `PATH`, or in `/<root of the project>/bin/<platform>`.
- To install `wat2wasm` using your favorite package manager, the name of the package is usually `wabt` (`apt install wabt`, `pacman -Sy wabt`, `brew install wabt`, etc). If you are not on linux or MacOS, you can download it here: <https://github.com/WebAssembly/wabt/releases/tag/1.0.31>
- To install `wasmtime` use the following command (on linux and MacOS): `curl https://wasmtime.dev/install.sh -sSf | bash`. If you are not on linux or MacOS, you can download it here: <https://docs.wasmtime.dev/cli-install.html>
## WebAssembly and Amy
Here you have some resources to get you started with WebAssembly:
- [WebAssembly demo](./material/webassembly-extra.md)
- Presentation by Georg Schmid from a few years ago: [Video](https://mediaspace.epfl.ch/media/09-10%2C+Code+Generation+Lab/0_8r1ahhhq/30820), [slides](https://lara.epfl.ch/~gschmid/clp20/codegen.pdf)
The lab has changed a tiny bit, for instance `set_global`, `get_global`, `set_local` and `get_local` are outdated and replaced with `global.set`, `global.get`, `local.set` and `local.get`, but otherwise it is a very good resource.
## The assignment code
### Overview
The code for the assignment is divided into two directories: `wasm` for the modeling of the WebAssembly framework, and `codegen` for Amy-specific code generation. There is a lot of code here, but your task is only to implement code generation for Amy expressions within `codegen/CodeGen.scala`.
- `wasm/Instructions.scala` provides types that describe a subset of WebAssembly instructions. It also provides a type `Code` to describe sequences of instructions. You can chain multiple instructions or `Code` objects together to generate a longer `Code` with the `<:>` operator.
- `wasm/Function.scala` describes a wasm function.
- `LocalsHandler` is an object which will create fresh indexes for local variables as needed.
- A `Function` contains a field called `isMain` which is used to denote a main function without a return value, which will be handled differently when printing.
- The only way to create a `Function` is using `Function.apply`. Its last argument is a function from a `LocalsHandler` to `Code`. The reason for this unusual choice is to make sure the `Function` object is instantiated with the number of local variables that will be requested from the LocalsHandler. To see how it is used, you can look in `codegen/Utils.scala` (but you won't have to use it directly).
- `wasm/Module.scala` and `wasm/ModulePrinter.scala` describe a wasm module, which you can think of as a set of functions and the corresponding module headers.
- `codegen/Utils.scala` contains a few utility functions (which you should use!) and implementations of the built-in functions of Amy. Use the builtins as examples. The builtins to read and write from and to StdIn and StdOut are particularly interesting; you might want to have a look at them (the comments are particularly insightful to understand the code).
- `codegen/CodeGen.scala` is the focus of the assignment. It contains code to translate Amy modules, functions and expressions to wasm code. It is a pipeline and returns a wasm Module.
- `codegen/CodePrinter.scala` is a Pipeline which will print output files from the wasm module.
### The cgExpr function
The focus of this assignment is the `cgExpr` function, which takes an expression and generates a `Code` object. It also takes two additional arguments: (1) a `LocalsHandler` which you can use to get a new slot for a local when you encounter a local variable or you need a temporary variable for your computation; (2) a map `locals` from `Identifiers` to locals slots, i.e. indices, in the wasm world. For example, if `locals` contains a pair `i -> 4`, we know that `local.get 4` in wasm will push the value of i to the stack. Notice how `locals` is instantiated with the function parameters in `cgFunction`.
## Skeleton
As usual, you can find the skeleton for this lab in a new branch of your group's repository. After merging it with your existing work, the structure of your project `src` directory should be as follows:
```plaintext
bin (new)
├── linux
│ ├── wasmtime
│ └── wat2wasm
├── macos
│ ├── wasmtime
│ └── wat2wasm
└── windows
├── wasmtime.exe
└── wat2wasm.exe
src/
├── amyc
│ ├── Main.scala (updated)
│ │
│ ├── analyzer
│ │ ├── SymbolTable.scala
│ │ ├── NameAnalyzer.scala
│ │ └── TypeChecker.scala
│ │
│ ├── ast
│ │ ├── Identifier.scala
│ │ ├── Printer.scala
│ │ └── TreeModule.scala
│ │
│ ├── codegen (new)
│ │ ├── CodeGen.scala
│ │ ├── CodePrinter.scala
│ │ └── Utils.scala
│ │
│ ├── interpreter
│ │ └── Interpreter.scala (to update with your own from lab01)
│ │
│ ├── lib
│ │ ├── scallion_3.0.6.jar
│ │ └── silex_3.0.6.jar
│ │
│ ├── parsing
│ │ ├── Parser.scala
│ │ ├── Lexer.scala
│ │ └── Tokens.scala
│ │
│ ├── utils
│ │ ├── AmycFatalError.scala
│ │ ├── Context.scala
│ │ ├── Document.scala
│ │ ├── Pipeline.scala
│ │ ├── Position.scala
│ │ ├── Reporter.scala
│ │ └── UniqueCounter.scala
│ │
│ └── wasm (new)
│ ├── Function.scala
│ ├── Instructions.scala
│ ├── ModulePrinter.scala
│ └── Module.scala
└── test
├── scala
│ └── amyc
│ └── test
│ ├── CodegenTests.scala
│ ├── CompilerTest.scala
│ ├── LexerTests.scala
│ ├── NameAnalyzerTests.scala
│ ├── ParserTests.scala
│ ├── TestSuite.scala
│ ├── TestUtils.scala
│ └── TyperTests.scala
└── resources
├── analyzer
│ └── ...
├── execution (new)
│ └── ...
├── lexer
│ └── ...
└── parser
└── ...
```
## Deliverables
Deadline: **02.05.2025 23:59:59**
You should submit the following files:
- `CodeGen.scala`: The implementation of the code generator.
File added
object L
abstract class List
case class Nil() extends List
case class Cons(h: Int(32), t: List) extends List
def isEmpty(l : List): Boolean = { l match {
case Nil() => true
case _ => false
}}
def length(l: List): Int(32) = { l match {
case Nil() => 0
case Cons(_, t) => 1 + length(t)
}}
def head(l: List): Int(32) = {
l match {
case Cons(h, _) => h
case Nil() => error("head(Nil)")
}
}
def headOption(l: List): O.Option = {
l match {
case Cons(h, _) => O.Some(h)
case Nil() => O.None()
}
}
def reverse(l: List): List = {
reverseAcc(l, Nil())
}
def reverseAcc(l: List, acc: List): List = {
l match {
case Nil() => acc
case Cons(h, t) => reverseAcc(t, Cons(h, acc))
}
}
def indexOf(l: List, i: Int(32)): Int(32) = {
l match {
case Nil() => -1
case Cons(h, t) =>
if (h == i) { 0 }
else {
val rec: Int(32) = indexOf(t, i);
if (0 <= rec) { rec + 1 }
else { -1 }
}
}
}
def range(from: Int(32), to: Int(32)): List = {
if (to < from) { Nil() }
else {
Cons(from, range(from + 1, to))
}
}
def sum(l: List): Int(32) = { l match {
case Nil() => 0
case Cons(h, t) => h + sum(t)
}}
def concat(l1: List, l2: List): List = {
l1 match {
case Nil() => l2
case Cons(h, t) => Cons(h, concat(t, l2))
}
}
def contains(l: List, elem: Int(32)): Boolean = { l match {
case Nil() =>
false
case Cons(h, t) =>
h == elem || contains(t, elem)
}}
abstract class LPair
case class LP(l1: List, l2: List) extends LPair
def merge(l1: List, l2: List): List = {
l1 match {
case Nil() => l2
case Cons(h1, t1) =>
l2 match {
case Nil() => l1
case Cons(h2, t2) =>
if (h1 <= h2) {
Cons(h1, merge(t1, l2))
} else {
Cons(h2, merge(l1, t2))
}
}
}
}
def split(l: List): LPair = {
l match {
case Cons(h1, Cons(h2, t)) =>
val rec: LPair = split(t);
rec match {
case LP(rec1, rec2) =>
LP(Cons(h1, rec1), Cons(h2, rec2))
}
case _ =>
LP(l, Nil())
}
}
def mergeSort(l: List): List = {
l match {
case Nil() => l
case Cons(h, Nil()) => l
case xs =>
split(xs) match {
case LP(l1, l2) =>
merge(mergeSort(l1), mergeSort(l2))
}
}
}
def toString(l: List): String = { l match {
case Nil() => "List()"
case more => "List(" ++ toString1(more) ++ ")"
}}
def toString1(l : List): String = { l match {
case Cons(h, Nil()) => Std.intToString(h)
case Cons(h, t) => Std.intToString(h) ++ ", " ++ toString1(t)
}}
def take(l: List, n: Int(32)): List = {
if (n <= 0) { Nil() }
else {
l match {
case Nil() => Nil()
case Cons(h, t) =>
Cons(h, take(t, n-1))
}
}
}
end L
object O
abstract class Option
case class None() extends Option
case class Some(v: Int(32)) extends Option
def isdefined(o: Option): Boolean = {
o match {
case None() => false
case _ => true
}
}
def get(o: Option): Int(32) = {
o match {
case Some(i) => i
case None() => error("get(None)")
}
}
def getOrElse(o: Option, i: Int(32)): Int(32) = {
o match {
case None() => i
case Some(oo) => oo
}
}
def orElse(o1: Option, o2: Option): Option = {
o1 match {
case Some(_) => o1
case None() => o2
}
}
def toList(o: Option): L.List = {
o match {
case Some(i) => L.Cons(i, L.Nil())
case None() => L.Nil()
}
}
end O
/** This module contains basic functionality for Amy,
* including stub implementations for some built-in functions
* (implemented in WASM or JavaScript)
*/
object Std
def printInt(i: Int(32)): Unit = {
printString(intToString(i))
}
def printString(s: String): Unit = {
error("") // Stub implementation
}
def printBoolean(b: Boolean): Unit = {
printString(booleanToString(b))
}
def readString(): String = {
error("") // Stub implementation
}
def readInt(): Int(32) = {
error("") // Stub implementation
}
def intToString(i: Int(32)): String = {
if (i < 0) {
"-" ++ intToString(-i)
} else {
val rem: Int(32) = i % 10;
val div: Int(32) = i / 10;
if (div == 0) { digitToString(rem) }
else { intToString(div) ++ digitToString(rem) }
}
}
def digitToString(i: Int(32)): String = {
error("") // Stub implementation
}
def booleanToString(b: Boolean): String = {
if (b) { "true" } else { "false" }
}
end Std
# Name Analysis
In the following, we will briefly discuss the purpose and implementation of the name analyzer phase in Amy. Name analysis has three goals:
* To reject programs that do not follow the Amy naming rules.
* For correct programs, to assign a unique identifier to every name. Remember that trees coming out of the parser contain plain strings wherever a name is expected. This might lead to confusion as to what each name refers to. Therefore, during name analysis, we assign a unique identifier to each name at its definition. Later in the program, every reference to that name will use the same unique identifier.
* To populate the symbol table. The symbol table contains a mapping from identifiers to all information that you could need later in the program for that identifier. For example, for each constructor, the symbol table contains an entry with the argument types, parent, and an index for this constructor.
After name analysis, only name-correct programs should survive, and they should contain unique identifiers that correspond to the correct symbol in the program.
## The Symbol Table
The symbol table contains information for all kinds of entities in the program. In the first half of name analysis, we discover all definitions of symbols, assign each of them a fresh identifier, and store these identifier-definition entries in the symbol table.
The `SymbolTable` API contains three kinds of methods:
* `addX` methods will add a new object to the symbol table. Among other things, these methods turn the strings found in nominal trees into the fresh `Identifier`s we will use to construct symbolic trees.
* `getX` methods which take an `Identifier` as an argument. This is what you will be using to resolve symbols you find in the program, for example, during type checking.
* `getX` methods which take two strings as arguments. These are only useful for name analysis and should not be used later: since during name analysis unique identifiers have not been assigned to everything from the start, sometimes our compiler will need to look up a definition based on its name and the name of its containing module. Of course you should not use these methods once you already have an identifier (in particular, not during type checking).
## The different tree modules
It is time to talk in detail about the different tree modules in the `TreeModule` file. As explained earlier, our goal is to define two very similar tree modules, with the only difference being how a (qualified) name is represented: In a *nominal* tree, i.e. one coming out of the parser, names are plain strings and qualified names are pairs of strings. On the other hand, in a *symbolic* tree, both kinds of names are unique identifiers.
To represent either kind of tree, we define a single Scala trait called `TreeModule` which defines two *abstract type fields* `Name` and `QualifiedName`. This trait also defines all types we need to represent Amy ASTs. Many of these types depend on the abstract types.
These abstract types are filled in when we instantiate the trait. Further down in the same file you can see that we define two objects `NominalTreeModule` and `SymbolicTreeModule`, which instantiate the abstract types. In addition all types within `TreeModule` are conceptually defined separately in each of the two implementations. As a result, there is a type called `NominalTreeModule.Ite` which is *different* from the type called `SymbolicTreeModule.Ite`.
## The NameAnalyzer class
The `NameAnalyzer` class implements Amy's naming rules (section 3.4 of the Amy specification). It takes a nominal program as an input and produces a symbol table and a symbolic program.
Name analysis is split into well-defined steps. The idea is the following: we first discover all definitions in the program in the correct order, i.e., modules, types, constructors, and, finally, functions. We then rewrite function bodies and expressions to refer to the newly-introduced identifiers.
Notice how name analysis takes as input the `NominalTreeModule.Program` output by the Parser, and returns a `SymbolicTreeModule.Program` along with a populated symbol table. During the last step we therefore transform the program and each of its subtrees from `NominalTreeModule.X` into `SymbolicTreeModule.X`. For instance, a `NominalTreeModule.Program` will be transformed into a `SymbolicTreeModule.Program`, a `NominalTreeModule.Ite` into a `SymbolicTreeModule.Ite` and so forth. To save some typing, we have imported NominalTreeModule as `N` and SymbolicTreeModule as `S`. So to refer e.g. to a `Plus` in the original (nominal) tree module we can simply use `N.Plus` -- to refer to one in the symbolic tree module we can use `S.Plus`.