Disabled external gits

This commit is contained in:
2022-04-07 18:43:21 +02:00
parent 182267a8cb
commit 88cb3426ad
1067 changed files with 102374 additions and 6 deletions

Submodule cs320-clp deleted from bc6ea60401

7
cs320-clp/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
bin
bin/*
wasmout
wasmout/*
/.idea/
/target/
/project/target/

Binary file not shown.

23
cs320-clp/build.sbt Normal file
View File

@@ -0,0 +1,23 @@
lazy val amyc = (project in file("."))
.disablePlugins(plugins.JUnitXmlReportPlugin)
.enablePlugins(StudentPlugin)
.settings(
name := "amyc",
version := "1.5",
organization := "ch.epfl.lara",
scalaVersion := "2.12.3",
scalaSource in Compile := baseDirectory.value / "src",
scalacOptions ++= Seq("-feature"),
scalacOptions += "-Xmixin-force-forwarders:false", //Fixes bug https://github.com/eclipse/lsp4j/issues/127
scalaSource in Test := baseDirectory.value / "test" / "scala",
parallelExecution in Test := false,
libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test",
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.1",
resolvers += "lsp4j-repository" at "https://download.eclipse.org/lsp4j/maven",
libraryDependencies += "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.8.1",
testOptions += Tests.Argument(TestFrameworks.JUnit, "-v")
)

View File

@@ -0,0 +1,36 @@
object Arithmetic {
def pow(b: Int, e: Int): Int = {
if (e == 0) { 1 }
else {
if (e % 2 == 0) {
val rec: Int = pow(b, e/2);
rec * rec
} else {
b * pow(b, e - 1)
}
}
}
def gcd(a: Int, b: Int): Int = {
if (a == 0 || b == 0) {
a + b
} else {
if (a < b) {
gcd(a, b % a)
} else {
gcd(a % b, b)
}
}
}
Std.printInt("");
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
}

View File

@@ -0,0 +1,34 @@
object Arithmetic {
def pow(b: Int, e: Int): Int = {
if (e == 0) { 1 }
else {
if (e % 2 == 0) {
val rec: Int = pow(b, e/2);
rec * rec
} else {
b * pow(b, e - 1)
}
}
}
def gcd(a: Int, b: Int): Int = {
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)); //
Std.printInt(gcd(16, 46)); // 2
Std.printInt(gcd(222, 888)) // 222
}

View File

@@ -0,0 +1,12 @@
object Factorial {
def fact(i: Int): Int = {
if (i < 2) { 1 }
else {
val rec: Int = fact(i-1);
i * rec
}
}
Std.printString("5! = " ++ Std.intToString(fact(5)));
Std.printString("10! = " ++ Std.intToString(fact(10)))
}

View File

@@ -0,0 +1,16 @@
object Hanoi {
def solve(n : Int) : Int = {
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)))
}

View File

@@ -0,0 +1,3 @@
object Hello {
Std.printString("Hello " ++ "world!")
}

View File

@@ -0,0 +1,7 @@
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 = Std.readInt();
Std.printString(Std.intToString(age) ++ " years old then.");
}

View File

@@ -0,0 +1,12 @@
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("" ++ "")
}

View File

@@ -0,0 +1,6 @@
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)))
}

View File

@@ -0,0 +1,36 @@
object Arithmetic {
def pow(b: Int, e: Int): Int = {
if (e == 0) { 1 }
else {
if (e % 2 == 0) {
val rec: Int = pow(b, e/2);
rec * rec
} else {
b * pow(b, e - 1)
}
}
}
def gcd(a: Int, b: Int): Int = {
if (a == 0 || b == 0) {
a + b
} else {
if (a < b) {
gcd(a, b % a)
} else {
gcd(a % b, b)
}
}
}
Std.printInt("");
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
}

View File

@@ -0,0 +1,38 @@
How to sublimetext:
1) install LSP package
2) Add this to user settings :
"clients":
{
"amycd":
{
"command":
[
"sbt",
"run"
],
"enabled": true,
"languages":
[
{
"languageId": "amyc",
"scopes":
[
"source.amy",
"source.scala"
],
"syntaxes":
[
"Packages/Scala/Scala.sublime-syntax"
]
}
]
}
},
Note: With the previous command/setting, you need to edit code in tha same project as the amyc language server (since it runs naivly and doesnt exist in the PATH or as an alias)
3) Open a .amy file
4) Set Syntax to scala
5) Start LSP server

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,144 @@
object L {
abstract class List
case class Nil() extends List
case class Cons(h: Int, t: List) extends List
def isEmpty(l : List): Boolean = { l match {
case Nil() => true
case _ => false
}}
def length(l: List): Int = { l match {
case Nil() => 0
case Cons(_, t) => 1 + length(t)
}}
def head(l: List): Int = {
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): Int = {
l match {
case Nil() => -1
case Cons(h, t) =>
if (h == i) { 0 }
else {
val rec: Int = indexOf(t, i);
if (0 <= rec) { rec + 1 }
else { -1 }
}
}
}
def range(from: Int, to: Int): List = {
if (to < from) { Nil() }
else {
Cons(from, range(from + 1, to))
}
}
def sum(l: List): Int = { 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): 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 l =>
split(l) 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): List = {
if (n <= 0) { Nil() }
else {
l match {
case Nil() => Nil()
case Cons(h, t) =>
Cons(h, take(t, n-1))
}
}
}
}

View File

@@ -0,0 +1,40 @@
object O {
abstract class Option
case class None() extends Option
case class Some(v: Int) extends Option
def isDefined(o: Option): Boolean = {
o match {
case None() => false
case _ => true
}
}
def get(o: Option): Int = {
o match {
case Some(i) => i
case None() => error("get(None)")
}
}
def getOrElse(o: Option, i: Int): Int = {
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()
}
}
}

View File

@@ -0,0 +1,40 @@
/** 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): Unit = {
error("") // Stub implementation
}
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 = {
error("") // Stub implementation
}
def intToString(i: Int): String = {
if (i < 0) {
"-" ++ intToString(-i)
} else {
val rem: Int = i % 10;
val div: Int = i / 10;
if (div == 0) { digitToString(rem) }
else { intToString(div) ++ digitToString(rem) }
}
}
def digitToString(i: Int): String = {
error("") // Stub implementation
}
def booleanToString(b: Boolean): String = {
if (b) { "true" } else { "false" }
}
}

View File

@@ -0,0 +1,98 @@
/** Adapted from student-build/project/StudentPlugin.scala in the moocs repo */
package ch.epfl.lara
import java.io.{File, FileInputStream, IOException}
import sbt.Keys._
import sbt._
/**
* Provides tasks for submitting the assignment
*/
object StudentPlugin extends AutoPlugin {
override lazy val projectSettings = Seq(
packageSubmissionSetting
) ++ packageSubmissionZipSettings
/** **********************************************************
* SUBMITTING A SOLUTION TO COURSERA
*/
val packageSourcesOnly = TaskKey[File]("packageSourcesOnly", "Package the sources of the project")
val packageSubmissionZip = TaskKey[File]("packageSubmissionZip")
val packageSubmissionZipSettings = Seq(
packageSubmissionZip := {
val submission = crossTarget.value / "submission.zip"
val sources = (packageSourcesOnly in Compile).value
val binaries = (packageBin in Compile).value
IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission)
submission
},
// Exclude resources from binaries
mappings in (Compile, packageBin) := {
val relativePaths =
(unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_))
(mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) }
},
artifactClassifier in packageSourcesOnly := Some("sources")
) ++ inConfig(Compile)(Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings))
val maxSubmitFileSize = {
val mb = 1024 * 1024
10 * mb
}
/** Check that the jar exists, isn't empty, isn't crazy big, and can be read
*/
def checkJar(jar: File, s: TaskStreams): Unit = {
val errPrefix = "Error submitting assignment jar: "
val fileLength = jar.length()
if (!jar.exists()) {
s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath)
failSubmit()
} else if (fileLength == 0L) {
s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath)
failSubmit()
} else if (fileLength > maxSubmitFileSize) {
s.log.error(errPrefix + "jar archive is too big. Allowed size: " +
maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" +
jar.getAbsolutePath)
failSubmit()
} else {
val bytes = new Array[Byte](fileLength.toInt)
val sizeRead = try {
val is = new FileInputStream(jar)
val read = is.read(bytes)
is.close()
read
} catch {
case ex: IOException =>
s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString)
failSubmit()
}
if (sizeRead != bytes.length) {
s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead)
failSubmit()
} else () // used to be: `encodeBase64(bytes)`
}
}
/** Task to package solution to a given file path */
val packageSubmission = inputKey[Unit]("package solution as an archive file")
lazy val packageSubmissionSetting = packageSubmission := {
val s: TaskStreams = streams.value // for logging
val jar = (packageSubmissionZip in Compile).value
checkJar(jar, s)
val path = baseDirectory.value / "submission.zip"
IO.copyFile(jar, path)
}
def failSubmit(): Nothing = {
sys.error("Packaging failed")
}
}

View File

@@ -0,0 +1 @@
sbt.version=1.3.5

BIN
cs320-clp/report/report.pdf Normal file

Binary file not shown.

BIN
cs320-clp/src/.DS_Store vendored Normal file

Binary file not shown.

BIN
cs320-clp/src/amyc/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,55 @@
package amyc
import java.io.File
import amyc.analyzer._
import amyc.ast._
import amyc.languageserver.LSLauncher
import amyc.parsing._
import amyc.utils._
object StdioLauncher {
}
object Main extends MainHelpers {
private def parseArgs(args: Array[String]): Context = {
Context(new Reporter, args.toList)
}
def main(args: Array[String]): Unit = {
println("Starting LS")
val lsl = new LSLauncher();
lsl.main(args)
}
}
trait MainHelpers {
import NominalTreeModule.{Program => NP}
import SymbolicTreeModule.{Program => SP}
def treePrinterS(title: String): Pipeline[(SP, SymbolTable), Unit] = {
new Pipeline[(SP, SymbolTable), Unit] {
def run(ctx: Context)(v: (SP, SymbolTable)) = {
println(title)
println(SymbolicPrinter(v._1)(true))
}
}
}
def treePrinterN(title: String): Pipeline[NP, Unit] = {
new Pipeline[NP, Unit] {
def run(ctx: Context)(v: NP) = {
println(title)
println(NominalPrinter(v))
}
}
}
}

View File

@@ -0,0 +1,283 @@
package amyc
package analyzer
import amyc.ast.{Identifier, NominalTreeModule => N, SymbolicTreeModule => S}
import amyc.utils._
// Name analyzer for Amy
// Takes a nominal program (names are plain strings, qualified names are string pairs)
// and returns a symbolic program, where all names have been resolved to unique Identifiers.
// Rejects programs that violate the Amy naming rules.
// Also populates and returns the symbol table.
object NameAnalyzer extends Pipeline[N.Program, (S.Program, SymbolTable)] {
def run(ctx: Context)(p: N.Program): (S.Program, SymbolTable) = {
import ctx.reporter._
// Step 0: Initialize symbol table
val table = new SymbolTable
// Step 1: Add modules to table
val modNames = p.modules.groupBy(_.name)
modNames.foreach { case (name, modules) =>
if (modules.size > 1) {
fatal(s"Two modules named $name in program", modules.head.position)
}
}
modNames.keys.toList foreach table.addModule
// Helper method: will transform a nominal type 'tt' to a symbolic type,
// given that we are within module 'inModule'.
def transformType(tt: N.TypeTree, inModule: String): S.Type = {
tt.tpe match {
case N.IntType => S.IntType
case N.BooleanType => S.BooleanType
case N.StringType => S.StringType
case N.UnitType => S.UnitType
case N.ClassType(qn@N.QualifiedName(module, name)) =>
table.getType(module getOrElse inModule, name) match {
case Some(symbol) =>
S.ClassType(symbol)
case None =>
fatal(s"Could not find type $qn", tt)
}
}
}
// Step 2: Check name uniqueness of definitions in each module
p.modules.foreach{
mod => mod.defs.groupBy(_.name).foreach {
case (name, ld) =>
if (ld.size > 1) fatal(s"Multiple definitions of module: $name in ${mod.name}")
}
}
// Step 3: Discover types and add them to symbol table
p.modules.foreach{
mod => mod.defs.foreach {
case N.AbstractClassDef(name) =>
table.addType(
mod.name,
name
)
case _ => //Ignore if not abstract-class
}
}
// Step 4: Discover type constructors, add them to table
p.modules.foreach{
mod => mod.defs.foreach {
case N.CaseClassDef(name, fields, parent) =>
table.addConstructor(
mod.name,
name,
fields.map(tt => transformType(tt, mod.name)),
table.getType(mod.name, parent).getOrElse(fatal(s"Non-existant: ${mod.name}"))
)
case _ => //Ignore if not case-class
}
}
// Step 5: Discover functions signatures, add them to table
p.modules.foreach {
mod => mod.defs.foreach{
case N.FunDef(name, params, rt, _) =>
table.addFunction(
mod.name,
name,
params.map(p => p.tt).map(tt => transformType(tt, mod.name)),
transformType(rt, mod.name)
)
case _ => //Ignore if not fun (note: last assignment was fun)
}
}
// Step 6: We now know all definitions in the program.
// Reconstruct modules and analyse function bodies/ expressions
// This part is split into three transfrom functions,
// for definitions, FunDefs, and expressions.
// Keep in mind that we transform constructs of the NominalTreeModule 'N' to respective constructs of the SymbolicTreeModule 'S'.
// transformFunDef is given as an example, as well as some code for the other ones
def transformDef(df: N.ClassOrFunDef, module: String): S.ClassOrFunDef = { df match {
case N.AbstractClassDef(name) =>
table.getType(module, name) match {
case Some(n)=> S.AbstractClassDef(n)
case None => fatal(s"Could not find class name")
}
case N.CaseClassDef(name, _, _) =>
table.getConstructor(module, name) match {
case Some(c)=>
S.CaseClassDef(c._1,c._2.argTypes.map(arg => S.TypeTree(arg)),c._2.parent)
case None => fatal(s"Could not find class name")
}
case fd: N.FunDef =>
transformFunDef(fd, module)
}}.setPos(df)
def transformFunDef(fd: N.FunDef, module: String): S.FunDef = {
val N.FunDef(name, params, retType, body) = fd
val Some((sym, sig)) = table.getFunction(module, name)
params.groupBy(_.name).foreach { case (name, ps) =>
if (ps.size > 1) {
fatal(s"Two parameters named $name in function ${fd.name}", fd)
}
}
val paramNames = params.map(_.name)
val newParams = params zip sig.argTypes map { case (pd@N.ParamDef(name, tt), tpe) =>
val s = Identifier.fresh(name)
S.ParamDef(s, S.TypeTree(tpe).setPos(tt)).setPos(pd)
}
val paramsMap = paramNames.zip(newParams.map(_.name)).toMap
S.FunDef(
sym,
newParams,
S.TypeTree(sig.retType).setPos(retType),
transformExpr(body)(module, (paramsMap, Map()))
).setPos(fd)
}
// This function takes as implicit a pair of two maps:
// The first is a map from names of parameters to their unique identifiers,
// the second is similar for local variables.
// Make sure to update them correctly if needed given the scoping rules of Amy
def transformExpr(expr: N.Expr)
(implicit module: String, names: (Map[String, Identifier], Map[String, Identifier])): S.Expr = {
val (params, locals) = names
val res : S.Expr = expr match {
// L1
case N.Let(df, value, body) =>
if (locals.contains(df.name))
fatal(s"Redefinition of variable ${df.name}", df.position)
val sn = Identifier.fresh(df.name)
S.Let(S.ParamDef(sn, S.TypeTree(transformType(df.tt, module))), transformExpr(value), transformExpr(body)(module, (params, locals + (df.name -> sn))))
case N.Sequence(s1, s2) => S.Sequence(transformExpr(s1),transformExpr(s2))
// L2
case N.Match(scrut, cases) =>
// Returns a transformed pattern along with all bindings
// from strings to unique identifiers for names bound in the pattern.
// Also, calls 'fatal' if a new name violates the Amy naming rules.
def transformPattern(pat: N.Pattern): (S.Pattern, List[(String, Identifier)]) = {
pat match{
case N.LiteralPattern(l) => l match{
case N.BooleanLiteral(x) => (S.LiteralPattern(S.BooleanLiteral(x)), List())
case N.IntLiteral(x) => (S.LiteralPattern(S.IntLiteral(x)), List())
case N.StringLiteral(x) => (S.LiteralPattern(S.StringLiteral(x)), List())
case N.UnitLiteral() => (S.LiteralPattern(S.UnitLiteral()), List())
}
case N.IdPattern(name) =>
if(locals.contains(name)){
fatal(s"Duplicate variable $name")
}
if(name == s"Nil"){
warning("Maybe you meant to write `Nil()` ?")
}
val id = Identifier.fresh(name)
(S.IdPattern(id), List((name,id)))
case N.WildcardPattern() => (S.WildcardPattern(), List())
case N.CaseClassPattern(p_name,args) =>
val owner = p_name.module.getOrElse(module)
val name = p_name.name
val constructor = table.getConstructor(owner,name).getOrElse(fatal(s"Constructor not found for Pattern $owner.$name"))
if(constructor._2.argTypes.size != args.size)
fatal(s"Invalid Arg Count for $owner.$name: had ${args.size}, expected ${constructor._2.argTypes.size}")
val n_args = args.map(arg => transformPattern(arg))
val pn_args = n_args.map(_._1)
val ln_args = n_args.flatMap(_._2)
ln_args.groupBy(_._1).foreach(el => {
if(el._2.size > 1)
fatal(s"Duplicate local variable ${el._1}")
if(locals.contains(el._1))
warning(s"Shadowing an existing variable ${el._1}")
})
(S.CaseClassPattern(constructor._1, pn_args),ln_args)
}
}
def transformCase(cse: N.MatchCase) : S.MatchCase = {
val N.MatchCase(pat, rhs) = cse
val (trans_pat, local_locals) = transformPattern(pat)
S.MatchCase(trans_pat,transformExpr(rhs)(module, (params,locals ++ local_locals)))
}
S.Match(transformExpr(scrut), cases.map(transformCase))
case N.Ite(cond, thenn, elze) => S.Ite(transformExpr(cond), transformExpr(thenn), transformExpr(elze))
// L3
case N.Plus(lhs,rhs) => S.Plus(transformExpr(lhs),transformExpr(rhs))
case N.Minus(lhs,rhs) => S.Minus(transformExpr(lhs),transformExpr(rhs))
case N.Times(lhs,rhs) => S.Times(transformExpr(lhs),transformExpr(rhs))
case N.Div(lhs,rhs) => S.Div(transformExpr(lhs),transformExpr(rhs))
case N.Mod(lhs,rhs) => S.Mod(transformExpr(lhs),transformExpr(rhs))
case N.LessThan(lhs,rhs) => S.LessThan(transformExpr(lhs),transformExpr(rhs))
case N.LessEquals(lhs,rhs) => S.LessEquals(transformExpr(lhs),transformExpr(rhs))
case N.And(lhs,rhs) => S.And(transformExpr(lhs),transformExpr(rhs))
case N.Or(lhs,rhs) => S.Or(transformExpr(lhs),transformExpr(rhs))
case N.Equals(lhs,rhs) => S.Equals(transformExpr(lhs),transformExpr(rhs))
case N.Concat(lhs,rhs) => S.Concat(transformExpr(lhs),transformExpr(rhs))
// L4
case N.Not(e) => S.Not(transformExpr(e))
case N.Neg(e) => S.Neg(transformExpr(e))
// L5
case N.IntLiteral(v) => S.IntLiteral(v)
case N.BooleanLiteral(v) => S.BooleanLiteral(v)
case N.StringLiteral(v) => S.StringLiteral(v)
case N.UnitLiteral() => S.UnitLiteral()
case N.Error(err) => S.Error(transformExpr(err))
case N.Variable(n) => S.Variable(locals.getOrElse(n, params.getOrElse(n, fatal(s"No variable $n", expr))))
case N.Call(qname, args) =>
val owner = qname.module.getOrElse(module)
val name = qname.name
val (sn, cs) = table.getConstructor(owner, name).getOrElse(table.getFunction(owner,name).getOrElse(fatal(s"No function found for $owner.$name")))
if (args.size != cs.argTypes.size)
fatal(s"Invalid Arg count for $owner.$name")
val fun_args = args.map(arg => transformExpr(arg))
S.Call(sn, fun_args)
//case _ =>// TODO: Implement the rest of the cases
}
res.setPos(expr)
}
// Putting it all together to construct the final program for step 6.
val newProgram = S.Program(
p.modules map { case mod@N.ModuleDef(name, defs, optExpr) =>
S.ModuleDef(
table.getModule(name).get,
defs map (transformDef(_, name)),
optExpr map (transformExpr(_)(name, (Map(), Map())))
).setPos(mod)
}
).setPos(p)
(newProgram, table)
}
}

View File

@@ -0,0 +1,85 @@
package amyc.analyzer
import amyc.ast.Identifier
import amyc.ast.SymbolicTreeModule._
import amyc.utils.UniqueCounter
import scala.collection.mutable.HashMap
trait Signature[RT <: Type]{
val argTypes: List[Type]
val retType: RT
}
// The signature of a function in the symbol table
case class FunSig(argTypes: List[Type], retType: Type, owner: Identifier) extends Signature[Type]
// The signature of a constructor in the symbol table
case class ConstrSig(argTypes: List[Type], parent: Identifier, index: Int) extends Signature[ClassType] {
val retType = ClassType(parent)
}
// A class that represents a dictionary of symbols for an Amy program
class SymbolTable {
private val defsByName = HashMap[(String, String), Identifier]()
private val modules = HashMap[String, Identifier]()
private val types = HashMap[Identifier, Identifier]()
private val functions = HashMap[Identifier, FunSig]()
private val constructors = HashMap[Identifier, ConstrSig]()
private val typesToConstructors = HashMap[Identifier, List[Identifier]]()
private val constrIndexes = new UniqueCounter[Identifier]
def addModule(name: String) = {
val s = Identifier.fresh(name)
modules += name -> s
s
}
def getModule(name: String) = modules.get(name)
def addType(owner: String, name: String) = {
val s = Identifier.fresh(name)
defsByName += (owner, name) -> s
types += (s -> modules.getOrElse(owner, sys.error(s"Module $name not found!")))
s
}
def getType(owner: String, name: String) =
defsByName.get(owner,name) filter types.contains
def getType(symbol: Identifier) = types.get(symbol)
def addConstructor(owner: String, name: String, argTypes: List[Type], parent: Identifier) = {
val s = Identifier.fresh(name)
defsByName += (owner, name) -> s
constructors += s -> ConstrSig(
argTypes,
parent,
constrIndexes.next(parent)
)
typesToConstructors += parent -> (typesToConstructors.getOrElse(parent, Nil) :+ s)
s
}
def getConstructor(owner: String, name: String): Option[(Identifier, ConstrSig)] = {
for {
sym <- defsByName.get(owner, name)
sig <- constructors.get(sym)
} yield (sym, sig)
}
def getConstructor(symbol: Identifier) = constructors.get(symbol)
def getConstructorsForType(t: Identifier) = typesToConstructors.get(t)
def addFunction(owner: String, name: String, argTypes: List[Type], retType: Type) = {
val s = Identifier.fresh(name)
defsByName += (owner, name) -> s
functions += s -> FunSig(argTypes, retType, getModule(owner).getOrElse(sys.error(s"Module $owner not found!")))
s
}
def getFunction(owner: String, name: String): Option[(Identifier, FunSig)] = {
for {
sym <- defsByName.get(owner, name)
sig <- functions.get(sym)
} yield (sym, sig)
}
def getFunction(symbol: Identifier) = functions.get(symbol)
}

View File

@@ -0,0 +1,175 @@
package amyc
package analyzer
import amyc.ast.Identifier
import amyc.ast.SymbolicTreeModule._
import amyc.utils._
// The type checker for Amy
// Takes a symbolic program and rejects it if it does not follow the Amy typing rules.
object TypeChecker extends Pipeline[(Program, SymbolTable), (Program, SymbolTable)] {
def run(ctx: Context)(v: (Program, SymbolTable)): (Program, SymbolTable) = {
import ctx.reporter._
val (program, table) = v
case class Constraint(found: Type, expected: Type, pos: Position)
// Represents a type variable.
// It extends Type, but it is meant only for internal type checker use,
// since no Amy value can have such type.
case class TypeVariable private (id: Int) extends Type
object TypeVariable {
private val c = new UniqueCounter[Unit]
def fresh(): TypeVariable = TypeVariable(c.next(()))
}
// Generates typing constraints for an expression `e` with a given expected type.
// The environment `env` contains all currently available bindings (you will have to
// extend these, e.g., to account for local variables).
// Returns a list of constraints among types. These will later be solved via unification.
def genConstraints(e: Expr, expected: Type)(implicit env: Map[Identifier, Type]): List[Constraint] = {
// This helper returns a list of a single constraint recording the type
// that we found (or generated) for the current expression `e`
def topLevelConstraint(found: Type): List[Constraint] =
List(Constraint(found, expected, e.position))
e match {
case IntLiteral(_) => topLevelConstraint(IntType)
case BooleanLiteral(_) => topLevelConstraint(BooleanType)
case StringLiteral(_) => topLevelConstraint(StringType)
case UnitLiteral() => topLevelConstraint(UnitType)
case Variable(name) => topLevelConstraint(env.getOrElse(name,UnitType))
case Plus(lhs, rhs) => topLevelConstraint(IntType) ++ genConstraints(lhs,IntType) ++ genConstraints(rhs,IntType)
case Minus(lhs, rhs) => topLevelConstraint(IntType) ++ genConstraints(lhs,IntType) ++ genConstraints(rhs,IntType)
case Times(lhs, rhs) => topLevelConstraint(IntType) ++ genConstraints(lhs,IntType) ++ genConstraints(rhs,IntType)
case Div(lhs, rhs) => topLevelConstraint(IntType) ++ genConstraints(lhs,IntType) ++ genConstraints(rhs,IntType)
case Mod(lhs, rhs) => topLevelConstraint(IntType) ++ genConstraints(lhs,IntType) ++ genConstraints(rhs,IntType)
case Neg(e) => topLevelConstraint(IntType) ++ genConstraints(e,IntType)
case Not(e) => topLevelConstraint(BooleanType) ++ genConstraints(e,BooleanType)
case LessThan(lhs, rhs) => topLevelConstraint(BooleanType) ++ genConstraints(lhs,IntType) ++ genConstraints(rhs,IntType)
case LessEquals(lhs, rhs) => topLevelConstraint(BooleanType) ++ genConstraints(lhs,IntType) ++ genConstraints(rhs,IntType)
case Equals(lhs, rhs) =>
val ltype = TypeVariable.fresh()
topLevelConstraint(BooleanType) ++ genConstraints(lhs,ltype) ++ genConstraints(rhs,ltype)
case And(lhs, rhs) => topLevelConstraint(BooleanType) ++ genConstraints(lhs,BooleanType) ++ genConstraints(rhs,BooleanType)
case Or(lhs, rhs) => topLevelConstraint(BooleanType) ++ genConstraints(lhs,BooleanType) ++ genConstraints(rhs,BooleanType)
case Concat(lhs, rhs) => topLevelConstraint(StringType) ++ genConstraints(lhs,StringType) ++ genConstraints(rhs,StringType)
case Error(msg) => topLevelConstraint(expected) ++ genConstraints(msg,StringType)
case Ite(cond, thenn, elze) =>
topLevelConstraint(expected) ++ genConstraints(cond,BooleanType) ++ genConstraints(thenn,expected) ++ genConstraints(elze,expected)
case Sequence(e1, e2) =>
topLevelConstraint(expected) ++ genConstraints(e1,TypeVariable.fresh()) ++ genConstraints(e2,expected)
case Let(df, value, body) =>
topLevelConstraint(expected) ++ genConstraints(value,df.tt.tpe) ++ genConstraints(body, expected)(env + (df.name -> df.tt.tpe))
case Call(qname, args) =>
val (sign, constr) = (table.getFunction(qname),table.getConstructor(qname)) match{
case (Some(s), None) => (s, Constraint(s.retType, expected, e.position))
case (None, Some(s)) => (s, Constraint(ClassType(s.parent), expected, e.position))
case _ => throw new MatchError("Invalid Case")
}
(constr :: (args.zip(sign.argTypes)).flatMap(p => genConstraints(p._1, p._2)))
case Match(scrut, cases) =>
// Returns additional constraints from within the pattern with all bindings
// from identifiers to types for names bound in the pattern.
// (This is analogous to `transformPattern` in NameAnalyzer.)
def handlePattern(pat: Pattern, scrutExpected: Type):
(List[Constraint], Map[Identifier, Type]) = pat match {
case CaseClassPattern(constr, args) =>
val constr_sig = table.getConstructor(constr).get
val in_pat = args.zip(constr_sig.argTypes).map(p => handlePattern(p._1, p._2))
(Constraint(ClassType(constr_sig.parent), scrutExpected, pat.position) ::
in_pat.flatMap(_._1), in_pat.flatMap(_._2.toList).toMap)
case IdPattern(name) => (List(), Map(name->scrutExpected))
case LiteralPattern(lit) => (genConstraints(lit,scrutExpected), Map())
case WildcardPattern() => (List(), Map())
}
def handleCase(cse: MatchCase, scrutExpected: Type): List[Constraint] = {
val (patConstraints, moreEnv) = handlePattern(cse.pat, scrutExpected)
patConstraints ++ genConstraints(cse.expr, expected)(env ++ moreEnv)
}
val st = TypeVariable.fresh()
genConstraints(scrut, st) ++ cases.flatMap(cse => handleCase(cse, st))
}
}
// Given a list of constraints `constraints`, replace every occurence of type variable
// with id `from` by type `to`.
def subst_*(constraints: List[Constraint], from: Int, to: Type): List[Constraint] = {
// Do a single substitution.
def subst(tpe: Type, from: Int, to: Type): Type = {
tpe match {
case TypeVariable(`from`) => to
case other => other
}
}
constraints map { case Constraint(found, expected, pos) =>
Constraint(subst(found, from, to), subst(expected, from, to), pos)
}
}
// Solve the given set of typing constraints and
// call `typeError` if they are not satisfiable.
// We consider a set of constraints to be satisfiable exactly if they unify.
def solveConstraints(constraints: List[Constraint]): Unit = {
constraints match {
case Nil => ()
case Constraint(found, expected, pos) :: more =>
// HINT: You can use the `subst_*` helper above to replace a type variable
// by another type in your current set of constraints.
(found, expected) match {
case (TypeVariable(id1), tpe@TypeVariable(id2)) =>
if (id1 == id2) {
solveConstraints(more)
} else {
solveConstraints(subst_*(constraints, id1, tpe))
}
case (tpe, TypeVariable(id)) =>
solveConstraints(subst_*(constraints, id, tpe))
case (tpe1, tpe2) =>
if(tpe1 == tpe2){
solveConstraints(more)
}else{
error(s"Error in TypeChecking, Types Not matching, found: ${found.toString}, expected ${expected.toString}.",pos)
solveConstraints(more)
}
}
}
}
// Putting it all together to type-check each module's functions and main expression.
program.modules.foreach { mod =>
// Put function parameters to the symbol table, then typecheck them against the return type
mod.defs.collect { case FunDef(_, params, retType, body) =>
val env = params.map{ case ParamDef(name, tt) => name -> tt.tpe }.toMap
solveConstraints(genConstraints(body, retType.tpe)(env))
}
// Type-check expression if present. We allow the result to be of an arbitrary type by
// passing a fresh (and therefore unconstrained) type variable as the expected type.
val tv = TypeVariable.fresh()
mod.optExpr.foreach(e => solveConstraints(genConstraints(e, tv)(Map())))
}
v
}
}

View File

@@ -0,0 +1,19 @@
package amyc.ast
object Identifier {
private val counter = new amyc.utils.UniqueCounter[String]
def fresh(name: String): Identifier = new Identifier(name)
}
// Denotes a unique identifier in an Amy program
// Notice that we rely on reference equality to compare Identifiers.
// The numeric id will be generated lazily,
// so the Identifiers are numbered in order when we print the program.
final class Identifier private(val name: String) {
private lazy val id = Identifier.counter.next(name)
def fullName = s"${name}_$id"
override def toString: String = name
}

View File

@@ -0,0 +1,201 @@
package amyc.ast
import amyc.utils._
import scala.language.implicitConversions
// A printer for Amy trees
trait Printer {
val treeModule: TreeModule
import treeModule._
implicit def printName(name: Name)(implicit printUniqueIds: Boolean): Document
implicit def printQName(name: QualifiedName)(implicit printUniqueIds: Boolean): Document
protected implicit def stringToDoc(s: String): Raw = Raw(s)
def apply(t: Tree)(implicit printUniqueIDs: Boolean = false): String = {
def binOp(e1: Expr, op: String, e2: Expr) = "(" <:> rec(e1) <:> " " + op + " " <:> rec(e2) <:> ")"
def rec(t: Tree, parens: Boolean = true): Document = t match {
/* Definitions */
case Program(modules) =>
Stacked(modules map (rec(_)), emptyLines = true)
case ModuleDef(name, defs, optExpr) =>
Stacked(
"object " <:> name <:> " {",
"",
Indented(Stacked(defs ++ optExpr.toList map (rec(_, false)), emptyLines = true)),
"}",
""
)
case AbstractClassDef(name) =>
"abstract class " <:> printName(name)
case CaseClassDef(name, fields, parent) =>
def printField(f: TypeTree) = "v: " <:> rec(f)
"case class " <:> name <:> "(" <:> Lined(fields map printField, ", ") <:> ") extends " <:> parent
case FunDef(name, params, retType, body) =>
Stacked(
"def " <:> name <:> "(" <:> Lined(params map (rec(_)), ", ") <:> "): " <:> rec(retType) <:> " = {",
Indented(rec(body, false)),
"}"
)
case ParamDef(name, tpe) =>
name <:> ": " <:> rec(tpe)
/* Expressions */
case Variable(name) =>
name
case IntLiteral(value) =>
value.toString
case BooleanLiteral(value) =>
value.toString
case StringLiteral(value) =>
'"' + value + '"'
case UnitLiteral() =>
"()"
case Plus(lhs, rhs) =>
binOp(lhs, "+", rhs)
case Minus(lhs, rhs) =>
binOp(lhs, "-", rhs)
case Times(lhs, rhs) =>
binOp(lhs, "*", rhs)
case Div(lhs, rhs) =>
binOp(lhs, "/", rhs)
case Mod(lhs, rhs) =>
binOp(lhs, "%", rhs)
case LessThan(lhs, rhs) =>
binOp(lhs, "<", rhs)
case LessEquals(lhs, rhs) =>
binOp(lhs, "<=", rhs)
case And(lhs, rhs) =>
binOp(lhs, "&&", rhs)
case Or(lhs, rhs) =>
binOp(lhs, "||", rhs)
case Equals(lhs, rhs) =>
binOp(lhs, "==", rhs)
case Concat(lhs, rhs) =>
binOp(lhs, "++", rhs)
case Not(e) =>
"!(" <:> rec(e) <:> ")"
case Neg(e) =>
"-(" <:> rec(e) <:> ")"
case Call(name, args) =>
name <:> "(" <:> Lined(args map (rec(_)), ", ") <:> ")"
case Sequence(lhs, rhs) =>
val main = Stacked(
rec(lhs, false) <:> ";",
rec(rhs, false),
)
if (parens) {
Stacked(
"(",
Indented(main),
")"
)
} else {
main
}
case Let(df, value, body) =>
val main = Stacked(
"val " <:> rec(df) <:> " =",
Indented(rec(value)) <:> ";",
rec(body, false) // For demonstration purposes, the scope or df is indented
)
if (parens) {
Stacked(
"(",
Indented(main),
")"
)
} else {
main
}
case Ite(cond, thenn, elze) =>
Stacked(
"(if(" <:> rec(cond) <:> ") {",
Indented(rec(thenn)),
"} else {",
Indented(rec(elze)),
"})"
)
case Match(scrut, cases) =>
Stacked(
rec(scrut) <:> " match {",
Indented(Stacked(cases map (rec(_)))),
"}"
)
case Error(msg) =>
"error(" <:> rec(msg) <:> ")"
/* cases and patterns */
case MatchCase(pat, expr) =>
Stacked(
"case " <:> rec(pat) <:> " =>",
Indented(rec(expr))
)
case WildcardPattern() =>
"_"
case IdPattern(name) =>
name
case LiteralPattern(lit) =>
rec(lit)
case CaseClassPattern(name, args) =>
name <:> "(" <:> Lined(args map (rec(_)), ", ") <:> ")"
/* Types */
case TypeTree(tp) =>
tp match {
case IntType => "Int"
case BooleanType => "Boolean"
case StringType => "String"
case UnitType => "Unit"
case ClassType(name) => name
}
}
rec(t).print
}
}
object NominalPrinter extends Printer {
val treeModule = NominalTreeModule
import NominalTreeModule._
implicit def printName(name: Name)(implicit printUniqueIds: Boolean): Document = Raw(name)
implicit def printQName(name: QualifiedName)(implicit printUniqueIds: Boolean): Document = {
Raw(name match {
case QualifiedName(Some(module), name) =>
s"$module.$name"
case QualifiedName(None, name) =>
name
})
}
}
object SymbolicPrinter extends SymbolicPrinter
trait SymbolicPrinter extends Printer {
val treeModule = SymbolicTreeModule
import SymbolicTreeModule._
implicit def printName(name: Name)(implicit printUniqueIds: Boolean): Document = {
if (printUniqueIds) {
name.fullName
} else {
name.name
}
}
@inline implicit def printQName(name: QualifiedName)(implicit printUniqueIds: Boolean): Document = {
printName(name)
}
}

View File

@@ -0,0 +1,141 @@
package amyc.ast
import amyc.utils.Positioned
/* A polymorphic module containing definitions of Amy trees.
*
* This trait represents either nominal trees (where names have not been resolved)
* or symbolic trees (where names/qualified names) have been resolved to unique identifiers.
* This is done by having two type fields within the module,
* which will be instantiated differently by the two different modules.
*
*/
trait TreeModule {
/* Represents the type for the name for this tree module.
* (It will be either a plain string, or a unique symbol)
*/
type Name
// Represents a name within an module
type QualifiedName
// A printer that knows how to print trees in this module.
// The modules will instantiate it as appropriate
val printer: Printer{ val treeModule: TreeModule.this.type }
// Common ancestor for all trees
trait Tree extends Positioned {
override def toString: String = printer(this)
}
// Expressions
trait Expr extends Tree
// Variables
case class Variable(name: Name) extends Expr
// Literals
trait Literal[+T] extends Expr { val value: T }
case class IntLiteral(value: Int) extends Literal[Int]
case class BooleanLiteral(value: Boolean) extends Literal[Boolean]
case class StringLiteral(value: String) extends Literal[String]
case class UnitLiteral() extends Literal[Unit] { val value: Unit = () }
// Binary operators
case class Plus(lhs: Expr, rhs: Expr) extends Expr
case class Minus(lhs: Expr, rhs: Expr) extends Expr
case class Times(lhs: Expr, rhs: Expr) extends Expr
case class Div(lhs: Expr, rhs: Expr) extends Expr
case class Mod(lhs: Expr, rhs: Expr) extends Expr
case class LessThan(lhs: Expr, rhs: Expr) extends Expr
case class LessEquals(lhs: Expr, rhs: Expr) extends Expr
case class And(lhs: Expr, rhs: Expr) extends Expr
case class Or(lhs: Expr, rhs: Expr) extends Expr
case class Equals(lhs: Expr, rhs: Expr) extends Expr
case class Concat(lhs: Expr, rhs: Expr) extends Expr
// Unary operators
case class Not(e: Expr) extends Expr
case class Neg(e: Expr) extends Expr
// Function/ type constructor call
case class Call(qname: QualifiedName, args: List[Expr]) extends Expr
// The ; operator
case class Sequence(e1: Expr, e2: Expr) extends Expr
// Local variable definition
case class Let(df: ParamDef, value: Expr, body: Expr) extends Expr
// If-then-else
case class Ite(cond: Expr, thenn: Expr, elze: Expr) extends Expr
// Pattern matching
case class Match(scrut: Expr, cases: List[MatchCase]) extends Expr {
require(cases.nonEmpty)
}
// Represents a computational error; prints its message, then exits
case class Error(msg: Expr) extends Expr
// Cases and patterns for Match expressions
case class MatchCase(pat: Pattern, expr: Expr) extends Tree
abstract class Pattern extends Tree
case class WildcardPattern() extends Pattern // _
case class IdPattern(name: Name) extends Pattern // x
case class LiteralPattern[+T](lit: Literal[T]) extends Pattern // 42, true
case class CaseClassPattern(constr: QualifiedName, args: List[Pattern]) extends Pattern // C(arg1, arg2)
// Definitions
trait Definition extends Tree { val name: Name }
case class ModuleDef(name: Name, defs: List[ClassOrFunDef], optExpr: Option[Expr]) extends Definition
trait ClassOrFunDef extends Definition
case class FunDef(name: Name, params: List[ParamDef], retType: TypeTree, body: Expr) extends ClassOrFunDef {
def paramNames = params.map(_.name)
}
case class AbstractClassDef(name: Name) extends ClassOrFunDef
case class CaseClassDef(name: Name, fields: List[TypeTree], parent: Name) extends ClassOrFunDef
case class ParamDef(name: Name, tt: TypeTree) extends Definition
// Types
trait Type
case object IntType extends Type {
override def toString: String = "Int"
}
case object BooleanType extends Type {
override def toString: String = "Boolean"
}
case object StringType extends Type {
override def toString: String = "String"
}
case object UnitType extends Type {
override def toString: String = "Unit"
}
case class ClassType(qname: QualifiedName) extends Type {
override def toString: String = printer.printQName(qname)(false).print
}
// A wrapper for types that is also a Tree (i.e. has a position)
case class TypeTree(tpe: Type) extends Tree
// All is wrapped in a program
case class Program(modules: List[ModuleDef]) extends Tree
}
/* A module containing trees where the names have not been resolved.
* Instantiates Name to String and QualifiedName to a pair of Strings
* representing (module, name) (where module is optional)
*/
object NominalTreeModule extends TreeModule {
type Name = String
case class QualifiedName(module: Option[String], name: String) {
override def toString: String = printer.printQName(this)(false).print
}
val printer = NominalPrinter
}
/* A module containing trees where the names have been resolved to unique identifiers.
* Both Name and ModuleName are instantiated to Identifier.
*/
object SymbolicTreeModule extends TreeModule {
type Name = Identifier
type QualifiedName = Identifier
val printer = SymbolicPrinter
}

View File

@@ -0,0 +1,152 @@
package amyc
package codegen
import amyc.analyzer._
import amyc.ast.Identifier
import amyc.ast.SymbolicTreeModule.{And => AmyAnd, Call => AmyCall, Div => AmyDiv, Or => AmyOr, _}
import amyc.codegen.Utils._
import amyc.utils.{Context, Pipeline}
import amyc.wasm.Instructions._
import amyc.wasm._
// Generates WebAssembly code for an Amy program
object CodeGen extends Pipeline[(Program, SymbolTable), Module] {
def run(ctx: Context)(v: (Program, SymbolTable)): Module = {
val (program, table) = v
// Generate code for an Amy module
def cgModule(moduleDef: ModuleDef): List[Function] = {
val ModuleDef(name, defs, optExpr) = moduleDef
// Generate code for all functions
defs.collect { case fd: FunDef if !builtInFunctions(fullName(name, fd.name)) =>
cgFunction(fd, name, false)
} ++
// Generate code for the "main" function, which contains the module expression
optExpr.toList.map { expr =>
val mainFd = FunDef(Identifier.fresh("main"), Nil, TypeTree(IntType), expr)
cgFunction(mainFd, name, true)
}
}
// Generate code for a function in module 'owner'
def cgFunction(fd: FunDef, owner: Identifier, isMain: Boolean): Function = {
// Note: We create the wasm function name from a combination of
// module and function name, since we put everything in the same wasm module.
val name = fullName(owner, fd.name)
Function(name, fd.params.size, isMain){ lh =>
val locals = fd.paramNames.zipWithIndex.toMap
val body = cgExpr(fd.body)(locals, lh)
if (isMain) {
body <:> Drop // Main functions do not return a value,
// so we need to drop the value generated by their body
} else {
body
}
}
}
// Generate code for an expression expr.
// Additional arguments are a mapping from identifiers (parameters and variables) to
// their index in the wasm local variables, and a LocalsHandler which will generate
// fresh local slots as required.
def cgExpr(expr: Expr)(implicit locals: Map[Identifier, Int], lh: LocalsHandler): Code = expr match {
case Let(df, value, body) =>
val address = lh.getFreshLocal()
cgExpr(value) <:> SetLocal(address) <:> cgExpr(body)(locals + (df.name -> address), lh)
case Variable(name) => GetLocal(locals(name))
case Concat(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Call(concatImpl.name)
case Sequence(expr1, expr2) => cgExpr(expr1) <:> Drop <:> cgExpr(expr2)
case Ite(condition, thenBlock, elseBlock) => cgExpr(condition) <:> If_i32 <:> cgExpr(thenBlock) <:> Else <:> cgExpr(elseBlock) <:> End
case Plus(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Add
case Minus(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Sub
case Times(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Mul
case AmyDiv(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Div
case Mod(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Rem
case Equals(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Eq
case LessEquals(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Le_s
case LessThan(lhs, rhs) => cgExpr(lhs) <:> cgExpr(rhs) <:> Lt_s
case AmyAnd(lhs, rhs) => cgExpr(lhs) <:> If_i32 <:> cgExpr(rhs) <:> Else <:> Const(0) <:> End
case AmyOr(lhs, rhs) => cgExpr(lhs) <:> If_i32 <:> Const(1) <:> Else <:> cgExpr(rhs) <:> End
case Neg(expr1) => Const(0) <:> cgExpr(expr1) <:> Sub
case Not(expr1) => cgExpr(expr1) <:> Eqz
case IntLiteral(lit) => Const(lit)
case StringLiteral(lit) => mkString(lit)
case BooleanLiteral(lit) => if (lit) Const(1) else Const(0)
case UnitLiteral() => Const(0)
case AmyCall(qname, args) => table.getConstructor(qname) match {
case Some(sig) => {
val newLocal = lh.getFreshLocal()
GetGlobal(memoryBoundary) <:> SetLocal(newLocal) <:>
GetGlobal(memoryBoundary) <:> adtField(args.size) <:> SetGlobal(memoryBoundary) <:>
GetLocal(newLocal) <:> Const(sig.index) <:> Store <:>
args.indices.map(i => GetLocal(newLocal) <:> adtField(i) <:> cgExpr(args(i)) <:> Store).toList <:>
GetLocal(newLocal)
}
case None => args.map(cgExpr) <:> Call(fullName(table.getFunction(qname).get.owner, qname))
}
case Match(scrut, cases) =>
def matchAndBind(pat: Pattern): (Code, Map[Identifier, Int]) = pat match {
case WildcardPattern() => (Drop <:> Const(1), Map.empty)
case IdPattern(name) =>
val idLocal = lh.getFreshLocal()
(SetLocal(idLocal) <:> Const(1), Map.empty + (name -> idLocal))
case LiteralPattern(lit) => (cgExpr(lit) <:> Eq, Map.empty)
case CaseClassPattern(constr, args) => {
val caseLocal = lh.getFreshLocal()
val argsLocalnCode = args.zipWithIndex.map(v => {
val mab = matchAndBind(v._1)
(GetLocal(caseLocal) <:> adtField(v._2) <:> Load <:> mab._1, mab._2)
})
val argsCode: Code = {
if (args.isEmpty) Const(1)
else if (args.lengthCompare(1) == 0) argsLocalnCode.map( _._1)
else argsLocalnCode.map( _._1) <:> args.tail.map(_ => And)
}
val idx = table.getConstructor(constr).get.index
(SetLocal(caseLocal) <:> GetLocal(caseLocal) <:> Load <:> Const(idx) <:> Eq <:>
If_i32 <:> argsCode <:> Else <:> Const(0) <:> End,
argsLocalnCode.map(_._2).foldLeft(Map.empty[Identifier, Int])(_ ++ _))
}
case _ => (Unreachable, Map.empty)
}
val newLocal = lh.getFreshLocal()
val caseCodes = cases.map(cse => {
val mnb = matchAndBind(cse.pat)
GetLocal(newLocal) <:> mnb._1 <:> If_i32 <:> cgExpr(cse.expr)(locals ++ mnb._2, lh) <:> Else
})
(cgExpr(scrut) <:> SetLocal(newLocal) <:> caseCodes <:> mkString("Match error!") <:> Call("Std_printString") <:> Unreachable <:> cases.map(_ => End))
case Error(msg) => cgExpr(StringLiteral("Error: ")) <:> cgExpr(msg) <:> Call(concatImpl.name) <:> Call("Std_printString") <:> Unreachable
}
Module(
program.modules.last.name.name,
defaultImports,
globalsNo,
wasmFunctions ++ (program.modules flatMap cgModule)
)
}
}

View File

@@ -0,0 +1,56 @@
package amyc.codegen
import java.io._
import amyc.utils.{Context, Env, Pipeline}
import amyc.wasm.Module
import scala.sys.process._
// Prints all 4 different files from a wasm Module
object CodePrinter extends Pipeline[Module, Unit]{
def run(ctx: Context)(m: Module) = {
val outDirName = "wasmout"
def withExt(ext: String) = s"$outDirName/${m.name}.$ext"
val (local, inPath) = {
import Env._
os match {
case Linux => ("./bin/wat2wasm", "wat2wasm")
case Windows => ("./bin/wat2wasm.exe", "wat2wasm.exe")
case Mac => ("./bin/mac/wat2wasm", "wat2wasm")
}
}
val w2wOptions = s"${withExt("wat")} -o ${withExt("wasm")}"
val outDir = new File(outDirName)
if (!outDir.exists()) {
outDir.mkdir()
}
m.writeWasmText(withExt("wat"))
try {
try {
s"$local $w2wOptions".!!
} catch {
case _: IOException =>
s"$inPath $w2wOptions".!!
}
} catch {
case _: IOException =>
ctx.reporter.fatal(
"wat2wasm utility was not found under ./bin or in system path, " +
"or did not have permission to execute"
)
case _: RuntimeException =>
ctx.reporter.fatal(s"wat2wasm failed to translate WebAssembly text file ${withExt("wat")} to binary")
}
m.writeHtmlWrapper(withExt("html"), withExt("wasm"))
m.writeNodejsWrapper(withExt("js"), withExt("wasm"))
}
}

View File

@@ -0,0 +1,158 @@
package amyc
package codegen
import amyc.ast.Identifier
import amyc.wasm.Function
import amyc.wasm.Instructions._
// Utilities for CodeGen
object Utils {
// The index of the global variable that represents the free memory boundary
val memoryBoundary: Int = 0
// # of global variables
val globalsNo = 1
// The default imports we will pass to a wasm Module
val defaultImports: List[String] = List(
"\"system\" \"printInt\" (func $Std_printInt (param i32) (result i32))",
"\"system\" \"printString\" (func $Std_printString (param i32) (result i32))",
"\"system\" \"readString0\" (func $js_readString0 (param i32) (result i32))",
"\"system\" \"readInt\" (func $Std_readInt (result i32))",
"\"system\" \"mem\" (memory 100)"
)
// We don't generate code for these functions in CodeGen (they are hard-coded here or in js wrapper)
val builtInFunctions: Set[String] = Set(
"Std_printInt",
"Std_printString",
"Std_digitToString",
"Std_readInt",
"Std_readString"
)
/** Utilities */
// A globally unique name for definitions
def fullName(owner: Identifier, df: Identifier): String = owner.name + "_" + df.name
// Given a pointer to an ADT on the top of the stack,
// will point at its field in index (and consume the ADT).
// 'index' MUST be 0-based.
def adtField(index: Int): Code = {
Const(4* (index + 1)) <:> Add
}
// Increment a local variable
def incr(local: Int): Code = {
GetLocal(local) <:> Const(1) <:> Add <:> SetLocal(local)
}
// A fresh label name
def getFreshLabel(name: String = "label") = {
Identifier.fresh(name).fullName
}
// Creates a known string constant s in memory
def mkString(s: String): Code = {
val size = s.length
val padding = 4 - size % 4
val completeS = s + 0.toChar.toString * padding
val setChars = for ((c, ind) <- completeS.zipWithIndex.toList) yield {
GetGlobal(memoryBoundary) <:> Const(ind) <:> Add <:>
Const(c.toInt) <:> Store8
}
val setMemory =
GetGlobal(memoryBoundary) <:> GetGlobal(memoryBoundary) <:> Const(size + padding) <:> Add <:>
SetGlobal(memoryBoundary)
setChars <:> setMemory
}
// Built-in implementation of concatenation
val concatImpl: Function = {
Function("String_concat", 2, false) { lh =>
val ptrS = lh.getFreshLocal()
val ptrD = lh.getFreshLocal()
val label = getFreshLabel()
def mkLoop: Code = {
val label = getFreshLabel()
Loop(label) <:>
// Load current character
GetLocal(ptrS) <:> Load8_u <:>
// If != 0
If_void <:>
// Copy to destination
GetLocal(ptrD) <:>
GetLocal(ptrS) <:> Load8_u <:>
Store8 <:>
// Increment pointers
incr(ptrD) <:> incr(ptrS) <:>
// Jump to loop
Br(label) <:>
Else <:>
End <:>
End
}
// Instantiate ptrD to previous memory, ptrS to first string
GetGlobal(memoryBoundary) <:>
SetLocal(ptrD) <:>
GetLocal(0) <:>
SetLocal(ptrS) <:>
// Copy first string
mkLoop <:>
// Set ptrS to second string
GetLocal(1) <:>
SetLocal(ptrS) <:>
// Copy second string
mkLoop <:>
//
// Pad with zeros until multiple of 4
//
Loop(label) <:>
// Write 0
GetLocal(ptrD) <:> Const(0) <:> Store8 <:>
// Check if multiple of 4
GetLocal(ptrD) <:> Const(4) <:> Rem <:>
// If not
If_void <:>
// Increment pointer and go back
incr(ptrD) <:>
Br(label) <:>
Else <:>
End <:>
End <:>
//
// Put string pointer to stack, set new memory boundary and return
GetGlobal(memoryBoundary) <:> GetLocal(ptrD) <:> Const(1) <:> Add <:> SetGlobal(memoryBoundary)
}
}
val digitToStringImpl: Function = {
Function("Std_digitToString", 1, false) { lh =>
// We know we have to create a string of total size 4 (digit code + padding), so we do it all together
// We do not need to shift the digit due to little endian structure!
GetGlobal(memoryBoundary) <:> GetLocal(0) <:> Const('0'.toInt) <:> Add <:> Store <:>
// Load memory boundary to stack, then move it by 4
GetGlobal(memoryBoundary) <:>
GetGlobal(memoryBoundary) <:> Const(4) <:> Add <:> SetGlobal(memoryBoundary)
}
}
val readStringImpl: Function = {
Function("Std_readString", 0, false) { lh =>
// We need to use the weird interface of javascript read string:
// we pass the old memory boundary and get the new one.
// In the end we have to return the old, where the fresh string lies.
GetGlobal(memoryBoundary) <:> GetGlobal(memoryBoundary) <:> Call("js_readString0") <:>
SetGlobal(memoryBoundary)
}
}
val wasmFunctions = List(concatImpl, digitToStringImpl, readStringImpl)
}

View File

@@ -0,0 +1,61 @@
package amyc.languageserver
import java.util
import org.eclipse.lsp4j.{CompletionOptions, InitializeParams, InitializeResult, MessageParams, MessageType, ServerCapabilities, TextDocumentSyncKind}
import org.eclipse.lsp4j.services.{LanguageClient, LanguageClientAware, LanguageServer, TextDocumentService, WorkspaceService}
import java.util.concurrent.CompletableFuture
class AmyServer extends LanguageServer with LanguageClientAware {
private val textDocumentService: TextDocumentService = new AmyTextDocumentService()
private val workspaceService: WorkspaceService = new AmyWorkspaceService()
private var client : LanguageClient = _
private var errorCode = 1
override def initialize(initializeParams: InitializeParams): CompletableFuture[InitializeResult] = {
sendLogMessageNotification(MessageType.Info, "Initializing the server...")
val initializeResult = new InitializeResult
val capabilities = new ServerCapabilities
capabilities.setTextDocumentSync(TextDocumentSyncKind.Full)
capabilities.setCompletionProvider(new CompletionOptions(true, util.Arrays.asList(".","?","&", "\"", "=")));
capabilities.setHoverProvider(false)
capabilities.setDefinitionProvider(true)
initializeResult.setCapabilities(capabilities)
CompletableFuture.completedFuture(initializeResult)
}
override def shutdown: CompletableFuture[AnyRef] = {
errorCode = 0
CompletableFuture.completedFuture(null)
}
override def exit(): Unit = {
System.exit(0)
}
override def getTextDocumentService: TextDocumentService = {
this.textDocumentService
}
override def getWorkspaceService: WorkspaceService = {
this.workspaceService
}
override def connect(client: LanguageClient): Unit = {
this.client = client
sendLogMessageNotification(MessageType.Info, "Connected to Language Server...")
}
def sendLogMessageNotification(tpe: MessageType, msg: String): Unit = {
client.logMessage(new MessageParams(tpe, msg))
}
def sendShowMessageNotification(tpe: MessageType, msg: String): Unit = {
client.showMessage(new MessageParams(tpe, msg))
}
}

View File

@@ -0,0 +1,172 @@
package amyc.languageserver
import java.io.File
import org.eclipse.lsp4j._
import org.eclipse.lsp4j.jsonrpc.messages.Either
import org.eclipse.lsp4j.TextDocumentItem
import org.eclipse.lsp4j.services.TextDocumentService
import org.eclipse.lsp4j.Hover
import java.util
import java.util.concurrent.CompletableFuture
import amyc.analyzer.{NameAnalyzer, TypeChecker}
import amyc.ast.SymbolicTreeModule
import amyc.parsing.{Lexer, Parser}
import amyc.utils.{Context, Reporter, SourcePosition}
class AmyTextDocumentService extends TextDocumentService {
/*
*/
private val openedDocuments : util.Map[String, TextDocumentItem] = new util.HashMap[String, TextDocumentItem]()
private def parseFile(file: Array[String]): Context = {
Context(new Reporter, file.toList)
}
override def completion(params: CompletionParams): CompletableFuture[Either[util.List[CompletionItem], CompletionList]] = {
// Provide completion item.
def AmyCompletionCompute(item: TextDocumentItem, pos: SourcePosition): util.List[CompletionItem] = {
val completionItems = new util.ArrayList[CompletionItem]
val completionItemPrintString = new CompletionItem
completionItemPrintString.setInsertText("Std.printString(\"\")")
completionItemPrintString.setLabel("Std.printString()")
completionItemPrintString.setKind(CompletionItemKind.Snippet)
completionItemPrintString.setDetail("Template to Print a String")
completionItems.add(completionItemPrintString)
val completionItemPrintInt = new CompletionItem
completionItemPrintInt.setInsertText("Std.printInt(\"\")")
completionItemPrintInt.setLabel("Std.printInt()")
completionItemPrintInt.setKind(CompletionItemKind.Snippet)
completionItemPrintInt.setDetail("Template to Print an Integer")
completionItems.add(completionItemPrintInt)
val pipeline = Lexer andThen Parser andThen NameAnalyzer //andThen TypeChecker
val ctx = parseFile(Array("./library/List.scala","./library/Option.scala", "./library/Std.scala",item.getUri))
val files = ctx.files.map(new File(_))
pipeline.run(ctx)(files)._1.modules.foreach(_.defs.foreach {
case SymbolicTreeModule.AbstractClassDef(name) =>
case SymbolicTreeModule.CaseClassDef(name, fields, parent) =>
case SymbolicTreeModule.FunDef(name, params, retType, body) =>
val completionItemPrintModule = new CompletionItem
if(params.nonEmpty) {
completionItemPrintModule.setInsertText(name.name + "(" +params.map(_.name.name).reduceLeft((s,p) => s + ", "+ p) + ")")
}
else {
completionItemPrintModule.setInsertText(name.name + "()")
}
completionItemPrintModule.setLabel(name.name)
completionItemPrintModule.setKind(CompletionItemKind.Snippet)
completionItemPrintModule.setDetail("Filled out call to " +name.name)
completionItems.add(completionItemPrintModule)
})
completionItems
}
CompletableFuture.supplyAsync(() => {
val textDocumentItem = openedDocuments.get(params.getTextDocument.getUri)
Either.forLeft(AmyCompletionCompute(textDocumentItem,
SourcePosition(null,
params.getPosition.getLine,
params.getPosition.getCharacter)))
})
}
override def resolveCompletionItem(item: CompletionItem): CompletableFuture[CompletionItem] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def hover(params: TextDocumentPositionParams): CompletableFuture[Hover] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def signatureHelp(params: TextDocumentPositionParams): CompletableFuture[SignatureHelp] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def definition(params: TextDocumentPositionParams): CompletableFuture[Either[util.List[_ <:Location],util.List[_<:LocationLink]]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def references(params: ReferenceParams): CompletableFuture[util.List[_ <:Location]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def documentHighlight(params: TextDocumentPositionParams): CompletableFuture[util.List[_ <:DocumentHighlight]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def documentSymbol(params: DocumentSymbolParams): CompletableFuture[util.List[Either[SymbolInformation,DocumentSymbol]]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def codeAction(params: CodeActionParams): CompletableFuture[util.List[Either[Command, CodeAction]]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def codeLens(params: CodeLensParams): CompletableFuture[util.List[_ <:CodeLens]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def resolveCodeLens(codeLens: CodeLens): CompletableFuture[CodeLens] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def formatting(params: DocumentFormattingParams): CompletableFuture[util.List[_ <:TextEdit]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def rangeFormatting(documentRangeFormattingParams: DocumentRangeFormattingParams): CompletableFuture[util.List[_ <:TextEdit]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def onTypeFormatting(documentOnTypeFormattingParams: DocumentOnTypeFormattingParams): CompletableFuture[util.List[_ <:TextEdit]] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def rename(renameParams: RenameParams): CompletableFuture[WorkspaceEdit] = {
//Not Implemented
CompletableFuture.completedFuture(null)
}
override def didOpen(didOpenTextDocumentParams: DidOpenTextDocumentParams): Unit = {
//Not Implemented
}
override def didChange(didChangeTextDocumentParams: DidChangeTextDocumentParams): Unit = {
//Not Implemented
}
override def didClose(didCloseTextDocumentParams: DidCloseTextDocumentParams): Unit = {
//Not Implemented
}
override def didSave(didSaveTextDocumentParams: DidSaveTextDocumentParams): Unit = {
//Not Implemented
}
}

View File

@@ -0,0 +1,25 @@
package amyc.languageserver
import java.util
import org.eclipse.lsp4j.{DidChangeConfigurationParams, DidChangeWatchedFilesParams, FileEvent, SymbolInformation, WorkspaceSymbolParams}
import org.eclipse.lsp4j.services.WorkspaceService
import java.util.concurrent.CompletableFuture
class AmyWorkspaceService extends WorkspaceService {
override def symbol(workspaceSymbolParams: WorkspaceSymbolParams): CompletableFuture[util.List[_ <:SymbolInformation]] = {
//Not Implemented
CompletableFuture.completedFuture(new util.ArrayList[SymbolInformation]())
}
override def didChangeConfiguration(params: DidChangeConfigurationParams): Unit = {
//Not Implemented
val settings: Any = params.getSettings
}
override def didChangeWatchedFiles(params: DidChangeWatchedFilesParams): Unit = {
//Not Implemented
val settings : util.List[FileEvent] = params.getChanges
}
}

View File

@@ -0,0 +1,24 @@
package amyc.languageserver
import java.net.Socket
import java.util.logging.{Level, LogManager, Logger}
import org.eclipse.lsp4j.launch.LSPLauncher
class LSLauncher {
def main(args: Array[String]): Unit = {
LogManager.getLogManager.reset()
val globalLogger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME)
globalLogger.setLevel(Level.OFF)
startLS()
}
private def startLS(): Unit = {
val ls = new AmyServer()
val launcher = LSPLauncher.createServerLauncher(ls, System.in, System.out)
ls.connect(launcher.getRemoteProxy)
launcher.startListening
}
}

View File

@@ -0,0 +1,158 @@
package amyc
package parsing
import java.io.File
import amyc.utils._
import scallion.input._
import scallion.lexical._
// The lexer for Amy.
object Lexer extends Pipeline[List[File], Iterator[Token]]
with Lexers[Token, Char, SourcePosition] {
/** Tiny Scallion-lexer reference:
* ==============================
* Scallion's lexer essentially allows you to define a list of regular expressions
* in their order of priority. To tokenize a given input stream of characters, each
* individual regular expression is applied in turn. If a given expression matches, it
* is used to produce a token of maximal length. Whenever a regular expression does not
* match, the expression of next-highest priority is tried.
* The result is a stream of tokens.
*
* Regular expressions `r` can be built using the following operators:
* - `word("abc")` matches the sequence "abc" exactly
* - `r1 | r2` matches either expression `r1` or expression `r2`
* - `r1 ~ r2` matches `r1` followed by `r2`
* - `oneof("xy")` matches either "x" or "y"
* (i.e., it is a shorthand of `word` and `|` for single characters)
* - `elem(c)` matches character `c`
* - `elem(f)` matches any character for which the boolean predicate `f` holds
* - `opt(r)` matches `r` or nothing at all
* - `many(r)` matches any number of repetitions of `r` (including none at all)
* - `many1(r)` matches any non-zero number of repetitions of `r`
*
* To define the token that should be output for a given expression, one can use
* the `|>` combinator with an expression on the left-hand side and a function
* producing the token on the right. The function is given the sequence of matched
* characters and the source-position range as arguments.
*
* For instance,
*
* `elem(_.isDigit) ~ word("kg") |> {
* (cs, range) => WeightLiteralToken(cs.mkString).setPos(range._1)) }`
*
* will match a single digit followed by the characters "kg" and turn them into a
* "WeightLiteralToken" whose value will be the full string matched (e.g. "1kg").
*/
import Tokens._
val lexer = Lexer(
// Keywords
word("abstract") | word("case") | word("class") |
word("def") | word("else") | word("extends") |
word("if") | word("match") | word("object") |
word("val") | word("error") | word("_")
|> { (cs, range) => KeywordToken(cs.mkString).setPos(range._1) },
//Type names
word("Int") | word("String") | word("Boolean") | word("Unit")
|> { (cs,range) => PrimTypeToken(cs.mkString).setPos(range._1) } ,
//Boolean literals
word("true")
|> { (cs,range)=> BoolLitToken(true).setPos(range._1) } ,
word("false")
|> { (cs,range)=> BoolLitToken(false).setPos(range._1) } ,
// Operators
oneOf("+-*/%!") |
word("||") | word("&&") |
word("<=") | word("<") |
word("==") | word("++")
|> { (cs,range )=> OperatorToken(cs.mkString).setPos(range._1) } ,
//Identifiers
elem(_.isLetter) ~ many(elem(c => { c.isLetter || c.isDigit || c == '_' }))
|> { (cs,range) => IdentifierToken(cs.mkString).setPos(range._1) } ,
//Integer literals
many(elem(_.isDigit))
|> { (cs,range)=> {
try{
IntLitToken(cs.mkString.toInt).setPos(range._1)
}catch{
case e: Exception => ErrorToken("Integer Error : "+e.toString+" at "+range._1).setPos(range._1)
}
}
},
// NOTE: Make sure to handle invalid (e.g. overflowing) integer values safely by
// emitting an ErrorToken instead.
// String literals
elem('\"') ~ many(elem((c)=> {c != '"' && c != '\n' && c!= '\r'} )) ~ elem('\"')
|> { (cs,range) => StringLitToken(cs.mkString.slice(1,cs.length -1 )).setPos(range._1) },
// NOTE: Not certain I should make an errorToken in case of unclosed strings...
// elem('"') ~ many(elem((c)=> {c != '"' && c != '\n' || c!= '\r'} ))
// |> {(cs,range) => ErrorToken("Unclosed String Error at "+range._1).setPos(range._1) },
// Delimiters and whitespace
word("=>") | oneOf(".,:;(){}[]=")
|> { (cs,range) => DelimiterToken(cs.mkString).setPos(range._1) },
elem(c => { c.isWhitespace })
|> { (cs,range) => SpaceToken().setPos(range._1) },
// Single line comments
word("//") ~ many(elem(_ != '\n'))
|> { cs => CommentToken(cs.mkString("")) },
// Multiline comments
word("/*") ~ many(elem(_ != '*') | elem('*')~elem(_ != '/') ) ~ word("*/")
|> { cs => CommentToken(cs.mkString("")) },
// NOTE: Amy does not support nested multi-line comments (e.g. `/* foo /* bar */ */`).
// Make sure that unclosed multi-line comments result in an ErrorToken.
word("/*")
|> { (cs,range) => ErrorToken("Unclosed Comment Error at "+range._1).setPos(range._1) },
) onError {
// We also emit ErrorTokens for Scallion-handled errors.
(cs, range) => ErrorToken(cs.mkString).setPos(range._1)
} onEnd {
// Once all the input has been consumed, we emit one EOFToken.
pos => EOFToken().setPos(pos)
}
override def run(ctx: Context)(files: List[File]): Iterator[Token] = {
var it = Seq[Token]().toIterator
for (file <- files) {
val source = Source.fromFile(file, SourcePositioner(file))
it ++= lexer.spawn(source).filter {
// Remove all whitespace and comment tokens
(token) => token match{
case CommentToken(_) => false
case SpaceToken() => false
case _ => true
}
}.map {
case token@ErrorToken(error) => ctx.reporter.fatal("Unknown token at " + token.position + ": " + error)
case token => token
}
}
it
}
}
/** Extracts all tokens from input and displays them */
object DisplayTokens extends Pipeline[Iterator[Token], Unit] {
override def run(ctx: Context)(tokens: Iterator[Token]): Unit = {
tokens.foreach(println(_))
}
}

View File

@@ -0,0 +1,274 @@
package amyc
package parsing
import amyc.ast.NominalTreeModule._
import amyc.parsing.TokenKinds._
import amyc.parsing.Tokens._
import amyc.utils._
import scallion.syntactic._
import scala.language.implicitConversions
// The parser for Amy
object Parser extends Pipeline[Iterator[Token], Program]
with Syntaxes[Token, TokenKind] with Debug[Token, TokenKind]
with Operators {
import Implicits._
override def getKind(token: Token): TokenKind = TokenKind.of(token)
val eof: Syntax[Token] = elem(EOFKind)
def op(string: String): Syntax[Token] = elem(OperatorKind(string))
def kw(string: String): Syntax[Token] = elem(KeywordKind(string))
implicit def delimiter(string: String): Syntax[Token] = elem(DelimiterKind(string))
// An entire program (the starting rule for any Amy file).
lazy val program: Syntax[Program] = many1(many1(module) ~<~ eof).map(ms => Program(ms.flatten.toList).setPos(ms.head.head))
// A module (i.e., a collection of definitions and an initializer expression)
lazy val module: Syntax[ModuleDef] = (kw("object") ~ identifier ~ "{" ~ many(definition) ~ opt(expr) ~ "}").map {
case obj ~ id ~ _ ~ defs ~ body ~ _ => ModuleDef(id, defs.toList, body).setPos(obj)
}
// An identifier.
val identifier: Syntax[String] = accept(IdentifierKind) {
case IdentifierToken(name) => name
}
// An identifier along with its position.
val identifierPos: Syntax[(String, Position)] = accept(IdentifierKind) {
case id@IdentifierToken(name) => (name, id.position)
}
// A definition within a module.
lazy val definition: Syntax[ClassOrFunDef] = fun_def | abstract_class_def | case_class_def
lazy val abstract_class_def : Syntax[ClassOrFunDef] = {
(kw("abstract") ~ kw("class") ~ identifierPos).map{
case v ~ _ ~ id => AbstractClassDef(id._1).setPos(v)
}
}
lazy val case_class_def : Syntax[ClassOrFunDef] = {
(kw("case") ~ kw("class") ~ identifier ~ "(" ~ parameters ~ ")" ~ kw("extends") ~ identifier).map{
case v ~ _ ~ id ~ _ ~ params ~ _ ~ _ ~ idp => CaseClassDef(id, params.map(_.tt), idp).setPos(v)
}
}
lazy val fun_def : Syntax[ClassOrFunDef]= {
(kw("def") ~ identifier ~ "(" ~ parameters ~ ")" ~ ":" ~ typeTree ~ "=" ~ "{" ~ expr ~ "}").map {
case v ~ id ~ _ ~ params ~ _ ~ _ ~ ret ~ _ ~ _ ~ body ~ _ => FunDef(id, params, ret, body).setPos(v)
}
}
// A list of parameter definitions.
lazy val parameters: Syntax[List[ParamDef]] = repsep(parameter, ",").map(_.toList)
// A parameter definition, i.e., an identifier along with the expected type.
lazy val parameter: Syntax[ParamDef] = {
(identifierPos ~ ":" ~ typeTree).map {
case id ~ _ ~ tt => ParamDef(id._1, tt).setPos(id._2)
}
}
// A type expression.
lazy val typeTree: Syntax[TypeTree] = primitiveType | identifierType
// A built-in type (such as `Int`).
val primitiveType: Syntax[TypeTree] = accept(PrimTypeKind) {
case tk@PrimTypeToken(name) => TypeTree(name match {
case "Unit" => UnitType
case "Boolean" => BooleanType
case "Int" => IntType
case "String" => StringType
case _ => throw new java.lang.Error("Unexpected primitive type name: " + name)
}).setPos(tk)
}
// A user-defined type (such as `List`).
lazy val identifierType: Syntax[TypeTree] = {
(identifierPos ~ opt("." ~ identifierPos)).map{
case id1 ~ Some(_ ~ id2) => TypeTree(ClassType(QualifiedName(Some(id1._1),id2._1))).setPos(id1._2)
case id1 ~ None => TypeTree(ClassType(QualifiedName(None, id1._1))).setPos(id1._2)
}
}
// An expression.
// HINT: You can use `operators` to take care of associativity and precedence
lazy val expr: Syntax[Expr] = recursive {
expr_l1
}
lazy val expr_l1: Syntax[Expr] = expr_ass | expr_seq
lazy val expr_l2: Syntax[Expr] = expr_match | expr_if
lazy val expr_l3: Syntax[Expr] = expr_op
lazy val expr_l4: Syntax[Expr] = expr_op_un
lazy val expr_l5: Syntax[Expr] = expr_err | expr_para | simpleExpr
lazy val expr_ass : Syntax[Expr] = {
(kw("val") ~ parameter ~ "=" ~ expr_l2 ~ ";" ~ expr).map {
case v ~ param ~ _ ~ expr1 ~ _ ~ expr2 => Let(param, expr1, expr2).setPos(v)
}
}
lazy val expr_seq : Syntax[Expr] = {
((expr_l2) ~ opt(";" ~ expr)).map{
case expr1 ~ Some(_ ~ expr2) => Sequence(expr1,expr2).setPos(expr1)
case expr1 ~ None => expr1.setPos(expr1)
}
}
lazy val expr_if : Syntax[Expr] = {
(kw("if") ~ "(" ~ expr ~ ")" ~ "{" ~ expr ~ "}" ~ kw("else") ~ "{" ~ expr ~ "}").map{
case v ~ _ ~ expr_iif ~ _ ~ _ ~ expr_then ~ _ ~ _ ~ _ ~ expr_else ~ _ => Ite(expr_iif, expr_then, expr_else).setPos(v)
}
}
lazy val expr_match : Syntax[Expr] = {
(expr_l3 ~ opt(kw("match") ~ "{" ~ many1(expr_case) ~ "}")).map{
case expr1 ~ Some(_ ~ _ ~ expr_c ~_) => Match(expr1, expr_c.toList).setPos(expr1)
case expr1 ~ None => expr1.setPos(expr1)
}
}
lazy val expr_case : Syntax[MatchCase] = {
(kw("case") ~ pattern ~ "=>" ~ expr).map{
case v ~ pat ~ _ ~ expr1 => MatchCase(pat,expr1).setPos(v)
}
}
lazy val expr_op : Syntax[Expr] = operation
lazy val expr_op_un : Syntax[Expr] = unary_operation
lazy val expr_err : Syntax[Expr] = {
(kw("error") ~ expr_para).map{
case v ~ err => Error(err).setPos(v)
}
}
lazy val expr_para : Syntax[Expr] = {
("(" ~ opt(expr) ~ ")").map {
case v ~ Some(expr1) ~ _ => expr1.setPos(v)
case _ ~ None ~ _ => UnitLiteral()
}
}
lazy val operation : Syntax[Expr] = operators(expr_l4)(
op("*") | op("/") | op("%") is LeftAssociative,
op("+") | op("-") | op("++") is LeftAssociative,
op("<") | op("<=") is LeftAssociative,
op("==") is LeftAssociative,
op("&&") is LeftAssociative,
op("||") is LeftAssociative
) {
case (lhs, OperatorToken("+"), rhs) => Plus(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("-"), rhs) => Minus(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("*"), rhs) => Times(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("/"), rhs) => Div(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("%"), rhs) => Mod(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("<"), rhs) => LessThan(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("<="), rhs) => LessEquals(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("&&"), rhs) => And(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("||"), rhs) => Or(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("=="), rhs) => Equals(lhs, rhs).setPos(lhs)
case (lhs, OperatorToken("++"), rhs) => Concat(lhs, rhs).setPos(lhs)
case _ => throw new java.lang.Error("Unexpected operation")
}
lazy val unary_operation : Syntax[Expr] = {
(opt( op("-") | op("!")) ~ expr_l5).map {
case Some(OperatorToken("-")) ~ expr1 => Neg(expr1)
case Some(OperatorToken("!")) ~ expr1 => Not(expr1)
case None ~ expr1 => expr1
case _ => throw new java.lang.Error("Unexpected unary operator")
}
}
// A literal expression.
lazy val literal: Syntax[Literal[_]] = {
accept(LiteralKind){
case tk@BoolLitToken(b) => BooleanLiteral(b).setPos(tk)
case tk@IntLitToken(i) => IntLiteral(i).setPos(tk)
case tk@StringLitToken(s) => StringLiteral(s).setPos(tk)
}
}
// A pattern as part of a mach case.
lazy val pattern: Syntax[Pattern] = recursive {
literalPattern | wildPattern | ioc_pat | unitPattern
}
lazy val ioc_pat : Syntax[Pattern] = {
(identifierPos ~ opt("." ~ identifierPos) ~ opt("(" ~ patterns ~")")).map{
case id1 ~ Some(_ ~ id2) ~ Some(_ ~ pats ~ _) => CaseClassPattern(QualifiedName(Some(id1._1), id2._1), pats)
case id1 ~ None ~ Some(_ ~ pats ~ _) => CaseClassPattern(QualifiedName(None, id1._1), pats)
case id ~ None ~ None => IdPattern(id._1).setPos(id._2)
case _ => throw new java.lang.Error("Unexpected pattern")
}
}
lazy val patterns: Syntax[List[Pattern]] = repsep(pattern,",").map(_.toList)
lazy val literalPattern: Syntax[Pattern] = {
literal.map{
v => LiteralPattern(v).setPos(v)
}
}
lazy val wildPattern: Syntax[Pattern] = {
kw("_").map{
v => WildcardPattern().setPos(v);
}
}
lazy val unitPattern: Syntax[Pattern] = {
("(" ~ ")").map(_ => LiteralPattern(UnitLiteral()) )
}
// HINT: It is useful to have a restricted set of expressions that don't include any more operators on the outer level.
lazy val simpleExpr: Syntax[Expr] = literal.up[Expr] | variableOrCall
lazy val variableOrCall: Syntax[Expr] = recursive{
(identifierPos ~ opt(opt("." ~ identifierPos) ~"(" ~ funArgs ~ ")")).map{
case id ~ None => Variable(id._1).setPos(id._2)
case id1 ~ Some(Some(_ ~ id2) ~ _ ~ params ~ _) => Call(QualifiedName(Some(id1._1), id2._1),params).setPos(id1._2)
case id1 ~ Some(None ~ _ ~ params ~ _) => Call(QualifiedName(None, id1._1),params).setPos(id1._2)
case _ => throw new java.lang.Error("Unexpected variable def")
}
}
lazy val funArgs: Syntax[List[Expr]] = repsep(expr, ",").map(_.toList)
// Ensures the grammar is in LL(1), otherwise prints some counterexamples
lazy val checkLL1: Boolean = {
if (program.isLL1) {
true
} else {
debug(program,true)
false
}
}
override def run(ctx: Context)(tokens: Iterator[Token]): Program = {
import ctx.reporter._
if (!checkLL1) {
ctx.reporter.fatal("Program grammar is not LL1!")
}
program(tokens) match {
case Parsed(result, rest) => result
case UnexpectedEnd(rest) => fatal("Unexpected end of input.")
case UnexpectedToken(token, rest) => fatal("Unexpected token: " + token + ", possible kinds: " + rest.first.map(_.toString).mkString(", "))
}
}
}

View File

@@ -0,0 +1,58 @@
package amyc
package parsing
import amyc.utils.Positioned
sealed trait Token extends Positioned with Product {
override def toString = {
productPrefix + productIterator.mkString("(", ",", ")") + "(" + position.withoutFile + ")"
}
}
object Tokens {
final case class KeywordToken(value: String) extends Token // e.g. keyword "if"
final case class IdentifierToken(name: String) extends Token // e.g. variable name "x"
final case class PrimTypeToken(value: String) extends Token // e.g. primitive type "Int"
final case class IntLitToken(value: Int) extends Token // e.g. integer literal "123"
final case class StringLitToken(value: String) extends Token
final case class BoolLitToken(value: Boolean) extends Token
final case class DelimiterToken(value: String) extends Token // .,:;(){}[]= and =>
final case class OperatorToken(name: String) extends Token // e.g. "+"
final case class CommentToken(text: String) extends Token // e.g. "// this is a comment"
final case class SpaceToken() extends Token // e.g. "\n "
final case class ErrorToken(content: String) extends Token
final case class EOFToken() extends Token // special token at the end of file
}
sealed abstract class TokenKind(representation: String) {
override def toString: String = representation
}
object TokenKinds {
final case class KeywordKind(value: String) extends TokenKind(value)
final case object IdentifierKind extends TokenKind("<Identifier>")
final case object PrimTypeKind extends TokenKind("<Primitive Type>")
final case object LiteralKind extends TokenKind("<Literal>")
final case class DelimiterKind(value: String) extends TokenKind(value)
final case class OperatorKind(value: String) extends TokenKind(value)
final case object EOFKind extends TokenKind("<EOF>")
final case object NoKind extends TokenKind("<???>")
}
object TokenKind {
import TokenKinds._
import Tokens._
def of(token: Token): TokenKind = token match {
case KeywordToken(value) => KeywordKind(value)
case IdentifierToken(_) => IdentifierKind
case PrimTypeToken(_) => PrimTypeKind
case BoolLitToken(_) => LiteralKind
case IntLitToken(_) => LiteralKind
case StringLitToken(_) => LiteralKind
case DelimiterToken(value) => DelimiterKind(value)
case OperatorToken(value) => OperatorKind(value)
case EOFToken() => EOFKind
case _ => NoKind
}
}

View File

@@ -0,0 +1,3 @@
package amyc.utils
case class AmycFatalError(msg: String) extends Exception(msg)

View File

@@ -0,0 +1,12 @@
package amyc.utils
// Contains a reporter and configuration for the compiler
case class Context(
reporter: Reporter,
files: List[String],
printTokens: Boolean = false,
printTrees: Boolean = false,
printNames: Boolean = false,
interpret: Boolean = false,
help: Boolean = false
)

View File

@@ -0,0 +1,49 @@
package amyc.utils
// A structured document to be printed with nice indentation
abstract class Document {
def <:>(other: Document) = Lined(List(this, other))
def print: String = {
val sb = new StringBuffer()
def rec(d: Document)(implicit ind: Int, first: Boolean): Unit = d match {
case Raw(s) =>
if (first && s.nonEmpty) sb append (" " * ind)
sb append s
case Indented(doc) =>
rec(doc)(ind + 1, first)
case Unindented(doc) =>
assume(ind > 0)
rec(doc)(ind - 1, first)
case Lined(Nil, _) => // skip
case Lined(docs, sep) =>
rec(docs.head)
docs.tail foreach { doc =>
rec(sep)(ind, false)
rec(doc)(ind, false)
}
case Stacked(Nil, _) => // skip
case Stacked(docs, emptyLines) =>
rec(docs.head)
docs.tail foreach { doc =>
sb append "\n"
if (emptyLines) sb append "\n"
rec(doc)(ind, true)
}
}
rec(this)(0, true)
sb.toString
}
}
case class Indented(content: Document) extends Document
case class Unindented(content: Document) extends Document
case class Stacked(docs: List[Document], emptyLines: Boolean = false) extends Document
case class Lined(docs: List[Document], separator: Document = Raw("")) extends Document
case class Raw(s: String) extends Document
object Stacked {
def apply(docs: Document*): Stacked = Stacked(docs.toList)
}

View File

@@ -0,0 +1,19 @@
package amyc.utils
object Env {
trait OS
object Linux extends OS
object Windows extends OS
object Mac extends OS
lazy val os = {
// If all fails returns Linux
val optOsName = Option(System.getProperty("os.name"))
optOsName.map(_.toLowerCase()).map { osName =>
if (osName contains "linux") Linux
else if (osName contains "win") Windows
else if (osName contains "mac") Mac
else Linux
} getOrElse Linux
}
}

View File

@@ -0,0 +1,21 @@
package amyc.utils
// A sequence of operations to be run by the compiler,
// with interruption at every stage if there is an error
abstract class Pipeline[-F, +T] {
self =>
def andThen[G](thenn: Pipeline[T, G]): Pipeline[F, G] = new Pipeline[F,G] {
def run(ctx : Context)(v : F) : G = {
val first = self.run(ctx)(v)
ctx.reporter.terminateIfErrors()
thenn.run(ctx)(first)
}
}
def run(ctx: Context)(v: F): T
}
case class Noop[T]() extends Pipeline[T, T] {
def run(ctx: Context)(v: T) = v
}

View File

@@ -0,0 +1,81 @@
package amyc.utils
import java.io.File
import scallion.input.Positioner
object Position {
/** Number of bits used to encode the line number */
private final val LINE_BITS = 20
/** Number of bits used to encode the column number */
private final val COLUMN_BITS = 31 - LINE_BITS // no negatives => 31
/** Mask to decode the line number */
private final val LINE_MASK = (1 << LINE_BITS) - 1
/** Mask to decode the column number */
private final val COLUMN_MASK = (1 << COLUMN_BITS) - 1
private def lineOf(pos: Int): Int = (pos >> COLUMN_BITS) & LINE_MASK
private def columnOf(pos: Int): Int = pos & COLUMN_MASK
def fromFile(f: File, i: Int) = {
SourcePosition(f, lineOf(i), columnOf(i))
}
}
abstract class Position {
val file: File
val line: Int
val col: Int
def isDefined: Boolean
def withoutFile: String
}
case class SourcePosition(file: File, line: Int, col: Int) extends Position {
override def toString: String = s"${file.getPath}:$line:$col"
def withoutFile = s"$line:$col"
val isDefined = true
}
case object NoPosition extends Position {
val file = null
val line = 0
val col = 0
override def toString: String = "?:?"
def withoutFile = toString
val isDefined = false
}
// A trait for entities which have a position in a file
trait Positioned {
protected var pos_ : Position = NoPosition
def hasPosition = pos_ != NoPosition
def position = pos_
def setPos(pos: Position): this.type = {
pos_ = pos
this
}
def setPos(other: Positioned): this.type = {
setPos(other.position)
}
}
case class SourcePositioner(file: File) extends Positioner[Char, SourcePosition] {
override val start: SourcePosition = SourcePosition(file, 1, 1)
override def increment(position: SourcePosition, character: Char): SourcePosition =
if (character == '\n') {
position.copy(line = position.line + 1, col = 1)
}
else {
position.copy(col = position.col + 1)
}
}

View File

@@ -0,0 +1,88 @@
package amyc.utils
import java.io.File
import scala.io.Source
// Reports errors and warnings during compilation
class Reporter {
/** Issues some information from the compiler */
def info(msg: Any, pos: Position = NoPosition): Unit = {
report("[ Info ]", msg, pos)
}
/** Issues a warning from the compiler */
def warning(msg: Any, pos: Position = NoPosition): Unit = {
report("[Warning]", msg, pos)
}
private var hasErrors = false
/** Issues a recoverable error message */
def error(msg: Any, pos: Position = NoPosition): Unit = {
hasErrors = true
report("[ Error ]", msg, pos)
}
/** Used for an unrecoverable error: Issues a message, then exits the compiler */
def fatal(msg: Any, pos: Position = NoPosition): Nothing = {
report("[ Fatal ]", msg, pos)
// Despite printing the message, we store it in the error for testing
val errMsg = s"$pos: $msg"
throw AmycFatalError(errMsg)
}
// Versions for Positioned
def info(msg: Any, pos: Positioned): Unit = info(msg, pos.position)
def warning(msg: Any, pos: Positioned): Unit = warning(msg, pos.position)
def error(msg: Any, pos: Positioned): Unit = error(msg, pos.position)
def fatal(msg: Any, pos: Positioned): Nothing = fatal(msg, pos.position)
/** Terminates the compiler if any errors have been detected. */
def terminateIfErrors() = {
if (hasErrors) {
fatal("There were errors.")
}
}
private def err(msg: String) {
Console.err.println(msg)
}
private def report(prefix: String, msg: Any, pos: Position) {
if (pos.isDefined) {
err(s"$prefix $pos: $msg")
val lines = getLines(pos.file)
if (pos.line > 0 && pos.line-1 < lines.size) {
err(s"$prefix ${lines(pos.line-1)}")
err(prefix + " " + " "*(pos.col - 1)+"^")
} else {
err(s"$prefix <line unavailable in source file>")
}
} else {
err(s"$prefix $msg")
}
}
private var filesToLines = Map[File, IndexedSeq[String]]()
private def getLines(f: File): IndexedSeq[String] = {
filesToLines.get(f) match {
case Some(lines) =>
lines
case None =>
val source = Source.fromFile(f).withPositioning(true)
val lines = source.getLines().toIndexedSeq
source.close()
filesToLines += f -> lines
lines
}
}
}

View File

@@ -0,0 +1,14 @@
package amyc.utils
import scala.collection.mutable
// Generates unique counters for each element of a type K
class UniqueCounter[K] {
private val elemIds = mutable.Map[K, Int]().withDefaultValue(-1)
def next(key: K): Int = synchronized {
elemIds(key) += 1
elemIds(key)
}
}

View File

@@ -0,0 +1,26 @@
package amyc.wasm
import amyc.wasm.Instructions.Code
// If isMain = false, represents a function which returns an i32 and will not be exported to js
// If isMain = true , represents a function which does not return a value, and will be exported to js
class Function private (val name: String, val args: Int, val isMain: Boolean, val locals: Int, val code: Code) {
override def toString: String = ModulePrinter(this)
}
class LocalsHandler(args: Int) {
private var locals_ = 0
def getFreshLocal(): Int = {
locals_ += 1
args + locals_ - 1
}
private[wasm] def locals = locals_
}
object Function {
def apply(name: String, args: Int, isMain: Boolean)(codeGen: LocalsHandler => Code) = {
val lh = new LocalsHandler(args)
// Make code first, as it may increment the locals in lh
val code = codeGen(lh)
new Function(name, args, isMain, lh.locals, code)
}
}

View File

@@ -0,0 +1,68 @@
package amyc
package wasm
import scala.language.implicitConversions
// A subset of instructions defined by the WASM standard
object Instructions {
abstract class Instruction
// Load an int32 constant to the stack
case class Const(value: Int) extends Instruction
// Numeric/logical instructions (all take i32 operands)
case object Add extends Instruction
case object Sub extends Instruction
case object Mul extends Instruction
case object Div extends Instruction
case object Rem extends Instruction
case object And extends Instruction
case object Or extends Instruction
case object Eqz extends Instruction // Return 1 if operand is 0, 0 otherwise
case object Lt_s extends Instruction // Signed less-than
case object Le_s extends Instruction // Signed less-equals
case object Eq extends Instruction
case object Drop extends Instruction // Drops the top value of the stack
// Control instructions
case object If_void extends Instruction // Marks the beginning of an if-block (with implicit 'then').
case object If_i32 extends Instruction // Marks the beginning of an if-block (with implicit 'then'). Must leave an i32 on the stack
case object Else extends Instruction // Marks the end of the implicit 'then' of an if-block
case object End extends Instruction // Marks the end of an if-then-else or block
case class Loop(label: String) extends Instruction // A block of instructions with a label at the beginning
case class Block(label: String) extends Instruction // A block of instructions with a label at the end
case class Br(label: String) extends Instruction // Jump to "label", which MUST be the label of an enclosing structure
case class Call(name: String) extends Instruction
case object Return extends Instruction
case object Unreachable extends Instruction // Always fails the program
// Locals (parameters, local variables)
case class GetLocal(index: Int) extends Instruction
case class SetLocal(index: Int) extends Instruction
// Global variables
case class GetGlobal(index: Int) extends Instruction
case class SetGlobal(index: Int) extends Instruction
// Memory
// Stores an i32 to memory. Expects memory address, then stored value as operands
case object Store extends Instruction
// Loads an i32 to memory. Expects memory address as operand
case object Load extends Instruction
// Stores a single byte to memory (the least significant byte of the operand)
// Operands expected are like Store
case object Store8 extends Instruction
// Load a byte from memory, then zero-extend it to fill an i32
case object Load8_u extends Instruction
// Represents a sequence of instructions
case class Code(instructions: List[Instruction]) {
def <:>(i: Instruction) = Code(instructions :+ i)
def <:>(other: Code) = Code(this.instructions ++ other.instructions)
}
// Useful implicit conversions to construct Code objects
implicit def i2c(i: Instruction): Code = Code(List(i))
implicit def is2c(is: List[Instruction]): Code = Code(is)
implicit def cs2c(cs: List[Code]): Code = Code(cs flatMap (_.instructions))
}

View File

@@ -0,0 +1,201 @@
package amyc
package wasm
// A WebAssembly module
case class Module(name: String, imports: List[String], globals: Int, functions: List[Function]) {
import java.io.{File, FileWriter}
def writeWasmText(fileName: String) = {
val fw = new FileWriter(new File(fileName))
fw.write(ModulePrinter(this))
fw.flush()
}
def writeHtmlWrapper(fileName: String, moduleFile: String) = {
val wrapperString =
s"""|<!doctype html>
|
|<html>
|
| <head>
| <meta charset="utf-8">
| <title>$name</title>
| </head>
|
| <body>
| <p id="htmlText"></p>
| <script>
|
| // This library function fetches the wasm module at 'url', instantiates it with
| // the given 'importObject', and returns the instantiated object instance
| // Taken from https://github.com/WebAssembly/spec
| function fetchAndInstantiate(url, importObject) {
| return fetch(url).then(response =>
| response.arrayBuffer()
| ).then(bytes =>
| WebAssembly.instantiate(bytes, importObject)
| ).then(results =>
| results.instance
| );
| }
|
| function writeHtml(line) {
| document.getElementById("htmlText").innerHTML += line + "<br>"
| }
|
| var memory = new WebAssembly.Memory({initial:100});
| var importObject = {
| system: {
| printInt: function(arg) {
| writeHtml(arg);
| 0;
| },
| printString: function(arg) {
| var bufView = new Uint8Array(memory.buffer);
| var i = arg;
| var result = "";
| while(bufView[i] != 0) {
| result += String.fromCharCode(bufView[i]);
| i = i + 1
| }
| writeHtml(result);
| 0;
| },
| mem: memory
| }
| };
|
| fetchAndInstantiate('$moduleFile', importObject).then(function(instance) {
|""".stripMargin ++
functions.filter(_.isMain).map { f =>
s" instance.exports.${f.name}();\n"
}.mkString ++
"""| });
| </script>
| </body>
|
|</html>
|
""".stripMargin
val fw = new FileWriter(new File(fileName))
fw.write(wrapperString)
fw.flush()
}
def writeNodejsWrapper(fileName: String, moduleFile: String): Unit = {
val wrapperString =
s"""function safe_require(module_name) {
| try {
| return require(module_name);
| } catch (e) {
| console.log('Error: nodejs module ' + module_name +
| ' must be installed (you might want to try: npm install ' + module_name + ')');
| process.exit(1);
| }
|}
|
|// `Wasm` does **not** understand node buffers, but thankfully a node buffer
|// is easy to convert to a native Uint8Array.
|function toUint8Array(buf) {
| var u = new Uint8Array(buf.length);
| for (var i = 0; i < buf.length; ++i) {
| u[i] = buf[i];
| }
| return u;
|}
|// Loads a WebAssembly dynamic library, returns a promise.
|// imports is an optional imports object
|function loadWebAssembly(filename, imports) {
| // Fetch the file and compile it
| const buffer = toUint8Array(require('fs').readFileSync(filename))
| return WebAssembly.compile(buffer).then(module => {
| return new WebAssembly.Instance(module, imports)
| })
|}
|
|var rl = require('readline').createInterface({
| input: process.stdin,
| output: process.stdout
|});
|
|function waitInput() {
| input = "";
| done = false;
| rl.resume();
| rl.on('line', function(answer) {
| rl.pause();
| input = answer;
| done = true
| });
| safe_require('deasync').loopWhile(function(){return !done;});
| return input;
|}
|
|var memory = new WebAssembly.Memory({initial:100});
|var importObject = {
| system: {
| mem: memory,
|
| printInt: function(arg) {
| console.log(arg);
| 0;
| },
|
| printString: function(arg) {
| var bufView = new Uint8Array(memory.buffer);
| var i = arg;
| var result = "";
| while(bufView[i] != 0) {
| result += String.fromCharCode(bufView[i]);
| i = i + 1
| }
| console.log(result);
| 0;
| },
|
| readInt: function() {
| var res = parseInt(waitInput());
| if (isNaN(res)) {
| console.log("Error: Could not parse int");
| process.exit(1)
| } else {
| return res
| }
| },
|
| // This function has a weird signature due to restrictions of the current WASM format:
| // It takes as argument the position that it needs to store the string to,
| // and returns the first position after the new string.
| readString0: function(memB) {
| var s = waitInput();
| var size = s.length;
| var padding = 4 - size % 4;
| var fullString = s + "\0".repeat(padding);
| var newMemB = memB + size + padding;
| var bufView8 = new Uint8Array(memory.buffer);
| for (var i = 0; i < fullString.length; i++) {
| bufView8[memB + i] = fullString.charCodeAt(i);
| }
| return newMemB;
| }
|
| }
|};
|
|loadWebAssembly('$moduleFile', importObject).then(function(instance) {
|""".stripMargin ++
functions.filter(_.isMain).map { f =>
s" instance.exports.${f.name}();\n"
}.mkString ++
""" rl.close();
|}).catch( function(error) {
| rl.close();
| process.exit(1)
|})
|""".stripMargin
val fw = new FileWriter(new File(fileName))
fw.write(wrapperString)
fw.flush()
}
}

View File

@@ -0,0 +1,107 @@
package amyc.wasm
import amyc.utils._
import amyc.wasm.Instructions._
import scala.language.implicitConversions
// Printer for Wasm modules
object ModulePrinter {
private implicit def s2d(s: String) = Raw(s)
private def mkMod(mod: Module): Document = Stacked(
"(module ",
Indented(Stacked(mod.imports map mkImport)),
Indented("(global (mut i32) i32.const 0) " * mod.globals),
Indented(Stacked(mod.functions map mkFun)),
")"
)
private def mkImport(s: String): Document =
Lined(List("(import ", s, ")"))
private def mkFun(fh: Function): Document = {
val name = fh.name
val isMain = fh.isMain
val exportDoc: Document = if (isMain) s"""(export "$name" (func $$$name))""" else ""
val paramsDoc: Document = if (fh.args == 0) "" else {
Lined(List(
"(param ",
Lined(List.fill(fh.args)(Raw("i32")), " "),
") "
))
}
val resultDoc: Document = if (isMain) "" else "(result i32) "
val localsDoc: Document =
if (fh.locals > 0)
"(local " <:> Lined(List.fill(fh.locals)(Raw("i32")), " ") <:> ")"
else
""
Stacked(
exportDoc,
Lined(List(s"(func $$${fh.name} ", paramsDoc, resultDoc, localsDoc)),
Indented(Stacked(mkCode(fh.code))),
")"
)
}
private def mkCode(code: Code): List[Document] = code.instructions match {
case Nil => Nil
case h :: t => h match {
case Else =>
Unindented(mkInstr(h)) ::
mkCode(t)
case End =>
Unindented(mkInstr(h)) ::
(mkCode(t) map Unindented)
case If_void | If_i32 | Block(_) | Loop(_) =>
mkInstr(h) ::
(mkCode(t) map Indented)
case _ =>
mkInstr(h) ::
mkCode(t)
}
}
private def mkInstr(instr: Instruction): Document = {
instr match {
case Const(value) => s"i32.const $value"
case Add => "i32.add"
case Sub => "i32.sub"
case Mul => "i32.mul"
case Div => "i32.div_s"
case Rem => "i32.rem_s"
case And => "i32.and"
case Or => "i32.or"
case Eqz => "i32.eqz"
case Lt_s => "i32.lt_s"
case Le_s => "i32.le_s"
case Eq => "i32.eq"
case Drop => "drop"
case If_void => "if"
case If_i32 => "if (result i32)"
case Else => "else"
case Block(label) => s"block $$$label"
case Loop(label) => s"loop $$$label"
case Br(label)=> s"br $$$label"
case Return => "ret"
case End => "end"
case Call(name) => s"call $$$name"
case Unreachable => "unreachable"
case GetLocal(index) => s"get_local $index"
case SetLocal(index) => s"set_local $index"
case GetGlobal(index) => s"get_global $index"
case SetGlobal(index) => s"set_global $index"
case Store => "i32.store"
case Load => "i32.load"
case Store8 => "i32.store8"
case Load8_u => "i32.load8_u"
}
}
def apply(mod: Module) = mkMod(mod).print
def apply(fh: Function) = mkFun(fh).print
def apply(instr: Instruction) = mkInstr(instr).print
}

BIN
cs320-clp/test/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
object MinimalError {
error("")
}

View File

@@ -0,0 +1,3 @@
object EmptyObject {
}

View File

@@ -0,0 +1,4 @@
/* This comment is closed twice,
which should not be interpreted as a single closed comment
*/*/

View File

@@ -0,0 +1 @@
&

View File

@@ -0,0 +1 @@
/***

View File

@@ -0,0 +1,3 @@
OperatorToken(*)(4:3)
OperatorToken(/)(4:4)
EOFToken()(4:5)

View File

@@ -0,0 +1,18 @@
KeywordToken(abstract)(1:1)
PrimTypeToken(Boolean)(1:10)
KeywordToken(case)(2:1)
KeywordToken(class)(2:6)
KeywordToken(def)(2:12)
KeywordToken(else)(2:16)
KeywordToken(error)(2:21)
KeywordToken(extends)(2:27)
BoolLitToken(false)(2:35)
KeywordToken(if)(2:41)
PrimTypeToken(Int)(2:44)
KeywordToken(match)(2:48)
KeywordToken(object)(2:54)
PrimTypeToken(String)(2:61)
BoolLitToken(true)(3:1)
PrimTypeToken(Unit)(3:6)
KeywordToken(val)(3:11)
EOFToken()(4:1)

View File

@@ -0,0 +1,3 @@
IntLitToken(1)(1:2)
IntLitToken(2)(2:5)
EOFToken()(3:1)

View File

@@ -0,0 +1,4 @@
/* This comment is closed twice,
which should be an error:
*/*/

View File

@@ -0,0 +1,3 @@
abstract Boolean
case class def else error extends false if Int match object String
true Unit val

View File

@@ -0,0 +1,2 @@
1 // Tab indented
2 // Space indented

View File

@@ -0,0 +1,3 @@
object Args {
def foo(i: Int): Int = { foo(1, 2) }
}

View File

@@ -0,0 +1,8 @@
object ParamAndLocal_0 {
def foo_0(i_0: Int): Int = {
val i_1: Int =
(i_0 + 1);
i_1
}
}

View File

@@ -0,0 +1,6 @@
object ParamAndLocal {
def foo(i: Int): Int = {
val i: Int = i + 1;
i
}
}

View File

@@ -0,0 +1,2 @@
object Empty {
}

View File

@@ -0,0 +1,11 @@
object IfCondition {
(if((
val x: Boolean =
true;
x
)) {
1
} else {
2
})
}

View File

@@ -0,0 +1,7 @@
object Literals {
1;
();
();
"Hello";
true
}

View File

@@ -0,0 +1,12 @@
object IfMatch {
(if(true) {
1
} else {
2
}) match {
case 1 =>
true
case 2 =>
false
}
}

View File

@@ -0,0 +1,2 @@
object Empty {
}

View File

@@ -0,0 +1,3 @@
object IfCondition {
if (val x: Boolean = true; x) { 1 } else { 2 }
}

View File

@@ -0,0 +1,3 @@
object Literals {
1; (); ( ); "Hello"; true
}

View File

@@ -0,0 +1,6 @@
object IfMatch {
if (true) { 1 } else { 2 } match {
case 1 => true
case 2 => false
}
}

View File

@@ -0,0 +1,3 @@
object ArithError1 {
1 + true
}

View File

@@ -0,0 +1,3 @@
object Arithmetic {
1 + 2 * 3 / 4 % -5
}

View File

@@ -0,0 +1,38 @@
package amyc.test
import java.io.ByteArrayInputStream
import amyc.analyzer.{NameAnalyzer, TypeChecker}
import amyc.codegen._
import amyc.parsing._
import amyc.utils._
import amyc.wasm.Module
import scala.collection.JavaConverters._
import scala.sys.process._
class CodegenTests extends ExecutionTests {
object CodePrinterExecutor extends Pipeline[Module, Unit] {
def run(ctx: Context)(m: Module) = {
CodePrinter.run(ctx)(m)
val fileName = s"${m.name}.js"
// Consume all standard input!
val input = Console.in.lines.iterator().asScala.toList.mkString("\n")
val inputS = new ByteArrayInputStream(input.getBytes("UTF-8"))
val exitCode = s"nodejs wasmout/$fileName" #< inputS ! ProcessLogger(Console.out.println, Console.err.println)
if (exitCode != 0)
throw AmycFatalError("Nonzero code returned from nodejs")
}
}
val pipeline =
Lexer andThen
Parser andThen
NameAnalyzer andThen
TypeChecker andThen
CodeGen andThen
CodePrinterExecutor
}

View File

@@ -0,0 +1,93 @@
package amyc.test
import java.io.File
import java.util.stream.Collectors
import amyc.utils._
import org.junit.Assert.fail
abstract class CompilerTest extends TestUtils {
private def runPipeline(pipeline: Pipeline[List[File], Unit], fileNames: List[String]) = {
val ctx = Context(new Reporter, fileNames)
val files = ctx.files.map(new File(_))
pipeline.run(ctx)(files)
ctx.reporter.terminateIfErrors()
}
private def runPipelineRedirected(
pipeline: Pipeline[List[File], Unit],
compiledFiles: List[String],
input: String
): String = {
testWithRedirectedIO(runPipeline(pipeline, compiledFiles), input)
}
private def assertEqual(output: String, expected: String) = {
val rejectLine = (s: String) =>
s.isEmpty ||
s.startsWith("[ Info ]") ||
s.startsWith("[Warning]") ||
s.startsWith("[ Error ]") ||
s.startsWith("[ Fatal ]")
def filtered(s: String) = s.lines.filter(rejectLine(_)==false).collect(Collectors.joining("\n"));
val filteredOutput = filtered(output)
val filteredExpected = filtered(expected)
if (filteredOutput != filteredExpected) {
val sb = new StringBuffer()
sb.append("\nOutput is different:\n")
sb.append("\nOutput: \n")
sb.append(filteredOutput)
sb.append("\n\nExpected output: \n")
sb.append(filteredExpected)
sb.append("\n")
fail(sb.toString)
}
}
protected def compareOutputs(
pipeline: Pipeline[List[File], Unit],
compiledFiles: List[String],
expectedFile: String,
input: String = ""
) = {
try {
val output = runPipelineRedirected(pipeline, compiledFiles, input)
val expected = scala.io.Source.fromFile(new File(expectedFile)).mkString
assertEqual(output, expected)
} catch {
// We only want to catch AmyFatalError gracefully, the rest can propagate
case AmycFatalError(msg) =>
fail(s"\n $msg\n")
}
}
protected def demandPass(
pipeline: Pipeline[List[File], Unit],
compiledFiles: List[String],
input: String = ""
) = {
try {
runPipelineRedirected(pipeline, compiledFiles, input)
} catch {
case AmycFatalError(msg) =>
fail(s"\n $msg\n")
}
}
protected def demandFailure(
pipeline: Pipeline[List[File], Unit],
compiledFiles: List[String],
input: String = ""
) = {
try {
runPipelineRedirected(pipeline, compiledFiles, input)
fail("Test should fail but it passed!")
} catch {
case AmycFatalError(_) =>
// Ok, this is what we wanted. Other exceptions should propagate though
}
}
}

View File

@@ -0,0 +1,15 @@
package amyc.test
import org.junit.Test
abstract class ExecutionTests extends TestSuite {
val baseDir = "interpreter"
val outputExt = "txt"
@Test def testEmptyObject = shouldOutput("EmptyObject")
@Test def testMinimalError = shouldFail("MinimalError")
}

View File

@@ -0,0 +1,17 @@
package amyc.test
import amyc.parsing._
import org.junit.Test
class LexerTests extends TestSuite {
val pipeline = Lexer andThen DisplayTokens
val baseDir = "lexer"
val outputExt = "txt"
@Test def testKeywords = shouldOutput("Keywords")
@Test def testSingleAmp = shouldFail("SingleAmp")
}

View File

@@ -0,0 +1,44 @@
package amyc.test
import amyc.analyzer.{NameAnalyzer, SymbolTable}
import amyc.ast.SymbolicTreeModule.Program
import amyc.ast.{Identifier, SymbolicPrinter}
import amyc.parsing._
import amyc.utils._
import org.junit.Test
import scala.language.implicitConversions
class NameAnalyzerTests extends TestSuite {
// A little hackery to overcome that Identifier names do not refresh over pipeline runs...
private class TestUniquePrinter extends SymbolicPrinter {
private val counter = new UniqueCounter[String]
private val map = scala.collection.mutable.Map[Identifier, Int]()
override implicit def printName(name: Identifier)(implicit printUniqueIds: Boolean): Document = {
if (printUniqueIds) {
val id = map.getOrElseUpdate(name, counter.next(name.name))
s"${name.name}_$id"
} else {
name.name
}
}
}
private val treePrinterS: Pipeline[(Program, SymbolTable), Unit] = {
new Pipeline[(Program, SymbolTable), Unit] {
def run(ctx: Context)(v: (Program, SymbolTable)) = {
println((new TestUniquePrinter)(v._1)(true))
}
}
}
val pipeline = Lexer andThen Parser andThen NameAnalyzer andThen treePrinterS
val baseDir = "nameAnalyzer"
val outputExt = "scala"
@Test def testParamAndLocal = shouldOutput("ParamAndLocal")
@Test def testArgumentNumberFunction = shouldFail("ArgumentNumberFunction")
}

View File

@@ -0,0 +1,18 @@
package amyc.test
import amyc.parsing._
import org.junit.Test
class ParserTests extends TestSuite with amyc.MainHelpers {
val pipeline = Lexer andThen Parser andThen treePrinterN("")
val baseDir = "parser"
val outputExt = "scala"
@Test def testEmpty = shouldOutput("Empty")
@Test def testLiterals = shouldOutput("Literals")
@Test def testEmptyFile = shouldFail("EmptyFile")
}

View File

@@ -0,0 +1,54 @@
package amyc.test
import java.io.File
import amyc.utils.Pipeline
abstract class TestSuite extends CompilerTest {
val pipeline: Pipeline[List[File], Unit]
val baseDir: String
lazy val effectiveBaseDir: String =
// getClass.getResource(s"/$baseDir").getPath
s"test/resources/$baseDir"
val passing = "passing"
val failing = "failing"
val outputs = "outputs"
val outputExt: String
def shouldOutput(inputFiles: List[String], outputFile: String, input: String = ""): Unit = {
compareOutputs(
pipeline,
inputFiles map (f => s"$effectiveBaseDir/$passing/$f.scala"),
s"$effectiveBaseDir/$outputs/$outputFile.$outputExt",
input
)
}
def shouldOutput(inputFile: String): Unit = {
shouldOutput(List(inputFile), inputFile)
}
def shouldFail(inputFiles: List[String], input: String = ""): Unit = {
demandFailure(
pipeline,
inputFiles map (f => s"$effectiveBaseDir/$failing/$f.scala"),
input
)
}
def shouldFail(inputFile: String): Unit = {
shouldFail(List(inputFile))
}
def shouldPass(inputFiles: List[String], input: String = ""): Unit = {
demandPass(pipeline, inputFiles map (f => s"$effectiveBaseDir/$passing/$f.scala"), input)
}
def shouldPass(inputFile: String): Unit = {
shouldPass(List(inputFile))
}
}

View File

@@ -0,0 +1,24 @@
package amyc.test
import java.io._
/** Some utilities for running tests */
trait TestUtils {
/** Run test,
* with input also redirected from a String,
* and output is redirected to a local StringBuilder.
*/
def testWithRedirectedIO[T](test: => T, input: String): String = {
import scala.Console._
val inputS = new StringReader(input)
val outputS = new ByteArrayOutputStream()
withOut(outputS) {
withErr(outputS) {
withIn(inputS) {
test
}
}
}
outputS.toString()
}
}

View File

@@ -0,0 +1,25 @@
package amyc.test
import amyc.analyzer.{NameAnalyzer, TypeChecker}
import amyc.parsing._
import amyc.utils._
import org.junit.Test
class TyperTests extends TestSuite {
// We need a unit pipeline
private def unit[A]: Pipeline[A, Unit] = {
new Pipeline[A, Unit] {
def run(ctx: Context)(v: A) = ()
}
}
val pipeline = Lexer andThen Parser andThen NameAnalyzer andThen TypeChecker andThen unit
val baseDir = "typer"
val outputExt = "" // No output files for typechecking
@Test def testArithError1 = shouldFail("ArithError1")
@Test def testArithmetic = shouldPass("Arithmetic")
}