initial commit

master
billsun 1 year ago
commit 8e8c90ec38

14
.gitignore vendored

@ -0,0 +1,14 @@
*
!src
!src/**
!gen
gen/*
!gen/bootstrap.c
!project
project/*
!project/build.properties
!*.scala
!.gitignore
!cleanall.sh
!build.sbt
!examples

@ -0,0 +1,12 @@
scalaVersion := "2.12.10"
scalacOptions in ThisBuild ++= Seq("-unchecked", "-deprecation", "-feature")
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test"
excludeFilter in unmanagedSources := HiddenFileFilter || "*sample*"
logBuffered in Test := false
parallelExecution in Test := false

@ -0,0 +1,3 @@
#!/bin/bash
find . -name target -type d -prune -exec rm -rf {} \;

@ -0,0 +1,5 @@
while (2 + 2) {
1
};
2

@ -0,0 +1 @@
if (2 > 4) 1 + 4 else 4

@ -0,0 +1,7 @@
var x = 2;
var y = 0;
while (y < 3) {
val dummy = x = x * x;
y = y + 1
};
x

@ -0,0 +1 @@
var x = 0; x = (x = x + 2) - 1

@ -0,0 +1,9 @@
#include <stdio.h>
// assembly function that we are compiling
int entry_point();
int main() {
printf("Result: %d\n", entry_point());
return 0;
}

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

@ -0,0 +1,277 @@
package project2
import scala.collection.mutable.HashMap
/*
* This compiler generates Scala-like code that computes the
* result of a given AST.
*/
abstract class StackCompiler extends BugReporter with Codegen {
import Language._
type Loc = Int
// Pretty printing
var nTab = 0
def emitln(msg: String): Unit = emitln(msg, nTab)
/**
* Env of the compiler. Keeps track of the location
* in memory of each variable defined.
*/
class Env {
def undef(name: String) = BUG(s"Undefined identifier $name (should have been found during the semantic analysis)")
def apply(name: String): Loc = undef(name)
}
case class LocationEnv(
vars: Map[String, Loc] = Map.empty,
outer: Env = new Env) extends Env {
/*
* Return a copy of the current state plus a
* variable 'name' at the location 'loc'
*/
def withVal(name: String, loc: Loc): LocationEnv = {
copy(vars = vars + (name -> loc))
}
/*
* Return the location of the variable 'name'
*/
override def apply(name: String): Loc = vars.get(name) match {
case Some(loc) => loc
case _ =>outer(name)
}
}
/*
* Generate code that computes the unary operator
* 'op' on the value at memory location 'sp' and that
* stores the result at 'sp'.
*/
def transUn(op: String)(sp: Loc) = op match {
case "-" => emitln(s"memory($sp) = -memory($sp)")
case "+" => ()
}
/*
* Generate code that computes the binary operator
* 'op' on the values at memory location 'sp' and
* 'sp1' and that stores the result at 'sp'.
*/
def transBin(op: String)(sp: Loc, sp1: Loc) = op match {
case "-" => emitln(s"memory($sp) -= memory($sp1)")
case "+" => emitln(s"memory($sp) += memory($sp1)")
case "*" => emitln(s"memory($sp) *= memory($sp1)")
case "/" => emitln(s"memory($sp) /= memory($sp1)")
case "%" => emitln(s"memory($sp) %= memory($sp1)")
}
/*
* Generate code that computes the binary operator
* 'op' on the values at memory location 'sp' and
* 'sp1' and that stores the result in 'flag'.
*/
def transCond(op: String)(sp: Loc, sp1: Loc) = op match {
case "==" => emitln(s"flag = (memory($sp) == memory($sp1))")
case "!=" => emitln(s"flag = (memory($sp) != memory($sp1))")
case "<=" => emitln(s"flag = (memory($sp) <= memory($sp1))")
case ">=" => emitln(s"flag = (memory($sp) >= memory($sp1))")
case "<" => emitln(s"flag = (memory($sp) < memory($sp1))")
case ">" => emitln(s"flag = (memory($sp) > memory($sp1))")
}
/*
* Generate code that computesS the result of the
* computation represented by the AST 'exp'.
*/
def emitCode(exp: Exp): Unit = {
emitln("type Val = Int")
emitln("val memory = new Array[Val](1000)")
emitln("var flag = true")
trans(exp, 0)(LocationEnv())
emitln(s"memory(0)")
}
/*
* Generate code that computes the result of the
* computation represented by the AST 'exp'. The
* value will be placed at memory location 'sp'
*/
def trans(exp: Exp, sp: Loc)(env: LocationEnv): Unit = exp match {
case Lit(x) =>
emitln(s"memory($sp) = $x")
case Unary(op, v) =>
trans(v, sp)(env)
transUn(op)(sp)
case Prim(op, lop, rop) =>
trans(lop, sp)(env)
trans(rop, sp + 1)(env)
transBin(op)(sp, sp + 1)
case Let(x, a, b) =>
trans(a, sp)(env)
trans(b, sp + 1)(env.withVal(x, sp))
emitln(s"memory($sp) = memory(${sp + 1})")
case Ref(x) =>
emitln(s"memory($sp) = memory(${env(x)})")
case Cond(op, l, r) =>
trans(l, sp)(env)
trans(r, sp+1)(env)
transCond(op)(sp, sp + 1)
case If(cond, tBranch, eBranch) =>
trans(cond, sp)(env) // Set flag value
emitln(s"if (flag) {")
nTab += 1
trans(tBranch, sp)(env)
nTab -= 1
emitln("} else {")
nTab += 1
trans(eBranch, sp)(env)
nTab -= 1
emitln("}")
case VarDec(x, rhs, body) =>
trans(rhs, sp)(env)
trans(body, sp + 1)(env.withVal(x, sp))
emitln(s"memory($sp) = memory(${sp + 1})")
case VarAssign(x, rhs) =>
trans(rhs, sp)(env)
emitln(s"memory(${env(x)}) = memory($sp)")
case While(cond, lBody, body) =>
trans(cond, sp)(env)
emitln(s"while (flag) {")
nTab += 1
trans(lBody, sp)(env)
trans(cond, sp)(env)
nTab -= 1
emitln(s"}")
trans(body, sp)(env)
}
}
abstract class X86Compiler extends BugReporter with Codegen {
import Language._
type Loc = Int
type PhyLoc = String
/**
* Env of the compiler. Keep track of the location
* in memory of each variable defined.
*/
private class Env {
def undef(name: String) = BUG(s"Undefined identifier $name (should have been found during the semantic analysis)")
def apply(name: String): Loc = undef(name)
}
private case class LocationEnv(
vars: Map[String, Loc] = Map.empty,
outer: Env = new Env) extends Env {
/*
* Return a copy of the current state plus a
* variable 'name' at the location 'loc'
*/
def withVal(name: String, loc: Loc): LocationEnv = {
copy(vars = vars + (name -> loc))
}
/*
* Return the location of the variable 'name'
*/
override def apply(name: String): Loc = vars.get(name) match {
case Some(loc) => loc
case _ => outer(name)
}
}
// List of available register.
val regs = Seq("%rbx", "%rcx", "%rdi", "%rsi", "%r8", "%r9", "%r10", "%r11", "%r12", "%r13", "%r14", "%r15")
/*
* Generate code that computes the unary operator
* 'op' on the value at memory location 'sp' and that
* stores the result at 'sp'.
*
* TODO: Fill in transUn with the appropriate code.
*/
def transUn(op: String)(sp: Loc) = op match {
case _ => BUG(s"Unary operator $op undefined")
}
/*
* Generate code that computes the binary operator
* 'op' on the values at memory location 'sp' and
* 'sp1' and that stores the result at 'sp'.
*/
def transBin(op: String)(sp: Loc, sp1: Loc) = op match {
case "+" => emitln(s"addq ${regs(sp1)}, ${regs(sp)}")
case "-" => emitln(s"subq ${regs(sp1)}, ${regs(sp)}")
case "*" => emitln(s"imulq ${regs(sp1)}, ${regs(sp)}")
case "/" =>
emitln(s"movq ${regs(sp)}, %rax")
emitln(s"cqto")
emitln(s"idiv ${regs(sp1)}")
emitln(s"movq %rax, ${regs(sp)}")
case _ => BUG(s"Binary operator $op undefined")
}
type Label = String
var nLabel = 0
def freshLabel(pref: String) = { nLabel += 1; s"$pref$nLabel" }
/*
* Generate code that jumps to the label 'label'
* if the CPU flags are verifying the operator
* 'op'
* NOTE: The flags will need to be set before.
*/
def transJumpIf(op: String)(label: Label) = {
op match {
case "==" => emitln(s"je $label")
case "!=" => emitln(s"jne $label")
case "<=" => emitln(s"jle $label")
case ">=" => emitln(s"jge $label")
case "<" => emitln(s"jl $label")
case ">" => emitln(s"jg $label")
}
}
/*
* Generate code that compute the result of the
* computation represented by the AST 'exp'.
*/
def emitCode(exp: Exp): Unit = {
trans(exp, 0)(LocationEnv())
emitln(s"movq ${regs(0)}, %rax")
}
/*
* Generate code that compute the result og the
* computation represented by the AST 'exp'. The
* value will be placed at memory location 'sp'
*
* TODO: Fill in each () with the appropriate code.
*/
def trans(exp: Exp, sp: Loc)(env: LocationEnv): Unit = exp match {
case Lit(x) =>
emitln(s"movq $$$x, ${regs(sp)}")
case Unary(op, v) => ()
case Prim(op, lop, rop) => ()
case Let(x, a, b) => ()
case Ref(x) => ()
case Cond(op, l, r) => ()
case If(cond, tBranch, eBranch) => ()
case VarDec(x, rhs, body) => ()
case VarAssign(x, rhs) => ()
case While(cond, lBody, body) =>
val lab = freshLabel("loop")
emitln(s"jmp ${lab}_cond")
emitln(s"${lab}_body:", 0)
() // TODO: continue ...
}
}

@ -0,0 +1,268 @@
package project2
import scala.collection.mutable.HashMap
abstract class Interpreter {
type Val
def run(ast: Language.Exp): Val
}
/**
* This interpreter specifies the semantics of our
* programming lanaguage.
*
* The evaluation of each node returns a value.
*/
class ValueInterpreter extends Interpreter with BugReporter {
import Language._
type Val = Int
/**
* Env of the interpreter. Keeps track of the value
* of each variable defined.
*/
class Env {
def undef(name: String) =
BUG(s"Undefined identifier $name (should have been found during the semantic analysis)")
def updateVar(name: String, v: Val): Val = undef(name)
def apply(name: String): Val = undef(name)
}
case class BoxedVal(var x: Val)
case class ValueEnv(
vars: Map[String, BoxedVal] = Map.empty,
outer: Env = new Env) extends Env {
/*
* Return a copy of the current state plus an immutable
* variable 'name' of value 'v'
*/
def withVal(name: String, v: Val): ValueEnv = {
copy(vars = vars + (name -> BoxedVal(v)))
}
/*
* Update the variable 'name' in this scope or in the
* outer scope.
* Return the new value of the variable
*/
override def updateVar(name: String, v: Val): Val = {
if (vars.contains(name))
vars(name).x = v
else
outer.updateVar(name, v)
v
}
/*
* Return the value of the variable 'name'
*/
override def apply(name: String): Val = {
if (vars.contains(name))
vars(name).x
else
outer(name)
}
}
/*
* Compute and return the result of the unary
* operation 'op' on the value 'v'
*/
def evalUn(op: String)(v: Val) = op match {
case "-" => -v
case "+" => v
}
/*
* Compute and return the result of the binary
* operation 'op' on the value 'v' and 'w'
* Note: v op w
*/
def evalBin(op: String)(v: Val, w: Val) = op match {
case "-" => v-w
case "+" => v+w
case "*" => v*w
case "/" => v/w
}
/*
* Compute and return the result of the condition
* operation 'op' on the value 'v' and 'w'
* Note: v op w
*/
def evalCond(op: String)(v: Val, w: Val) = op match {
case "==" => v == w
case "!=" => v != w
case "<=" => v <= w
case ">=" => v >= w
case "<" => v < w
case ">" => v > w
}
/*
* Evaluate the AST starting with an empty Env
*/
def run(exp: Exp) = eval(exp)(ValueEnv())
/*
* Evaluate the AST within the environment 'env'
*/
def eval(exp: Exp)(env: ValueEnv): Val = exp match {
case Lit(x) => x
case Unary(op, v) =>
evalUn(op)(eval(v)(env))
case Prim(op, lop, rop) =>
evalBin(op)(eval(lop)(env), eval(rop)(env))
case Let(x, a, b) =>
eval(b)(env.withVal(x, eval(a)(env)))
case Ref(x) =>
env(x)
case If(Cond(op, l, r), tBranch, eBranch) =>
if (evalCond(op)(eval(l)(env), eval(r)(env)))
eval(tBranch)(env)
else
eval(eBranch)(env)
case VarDec(x, rhs, body) =>
eval(body)(env.withVal(x, eval(rhs)(env)))
case VarAssign(x, rhs) =>
env.updateVar(x, eval(rhs)(env))
case While(Cond(op, l, r), lBody, body) =>
while (evalCond(op)(eval(l)(env), eval(r)(env))) {
eval(lBody)(env)
}
eval(body)(env)
}
}
/**
* This interpreter is a stack-based interpreter as we have seen
* during the lecture.
*
* Rather than returning the value of a node, it stores it in memory,
* following a well-establish convention.
*
* This interpreter works in a similar manner as a processor.
*/
class StackInterpreter extends Interpreter with BugReporter {
import Language._
type Val = Int
type Loc = Int
/**
* Env of the interpreter. Keep track of the location
* in memory of each variable defined.
*/
class Env {
def apply(name: String): Loc =
BUG(s"Undefined identifier $name (should have been found during the semantic analysis)")
}
case class LocationEnv(
vars: Map[String, Loc] = Map.empty,
outer: Env = new Env) extends Env {
/*
* Return a copy of the current state plus a
* variable 'name' at the location 'loc'
*/
def withVal(name: String, loc: Loc): LocationEnv = {
copy(vars = vars + (name -> loc))
}
/*
* Return the location of the variable 'name'
*/
override def apply(name: String) = vars.get(name) match {
case Some(loc) => loc
case None => outer(name)
}
}
/*
* Compute the result of the operator 'op' on the
* value stored at 'sp' and store it at 'sp'
*
* TODO: Implement the appropriate code as defined in the handout.
*/
def evalUn(op: String)(sp: Loc) = op match {
case _ => BUG(s"Unary operator $op undefined")
}
/*
* Compute the result of the operator 'op' on the
* value stored at 'sp' and 'sp1', and store it at 'sp'
*
* NOTE: sp op sp1
*/
def evalBin(op: String)(sp: Loc, sp1: Loc) = op match {
case "+" => memory(sp) += memory(sp1)
case "-" => memory(sp) -= memory(sp1)
case "*" => memory(sp) *= memory(sp1)
case "/" => memory(sp) /= memory(sp1)
}
/*
* Compute the result of the operator 'op' on the
* value stored at 'sp' and 'sp1', and store it in the
* variable 'flag'.
*
* NOTE: sp op sp1
*
* TODO: Implement the appropriate code as defined in the handout.
*/
def evalCond(op: String)(sp: Loc, sp1: Loc) = {
flag = op match {
case "==" => memory(sp) == memory(sp1)
case _ => BUG(s"Binary operator $op undefined")
}
}
// Memory and flag used by the interpreter
val memory = new Array[Val](1000)
var flag: Boolean = true
/*
* Evaluate the value of the AST 'exp' within
* an empty environment and return the value.
*/
def run(exp: Exp): Val = {
eval(exp, 0)(LocationEnv())
memory(0)
}
/*
* Evaluate the value of the AST 'exp' within
* the environment 'env 'and store the result
* at 'sp'.
*
* NOTE: Cond stores its result in the 'flag'
* variable.
*
* TODO: Remove all ???s and implement the
* appropriate code as defined in the handout.
*/
def eval(exp: Exp, sp: Loc)(env: LocationEnv): Unit = exp match {
case Lit(x) =>
memory(sp) = x
case Unary(op, v) => ???
case Prim(op, lop, rop) => ???
case Let(x, a, b) => ???
case Ref(x) => ???
case Cond(op, l, r) => ???
case If(cond, tBranch, eBranch) => ???
case VarDec(x, rhs, body) => ???
case VarAssign(x, rhs) => ???
case While(cond, lbody, body) =>
eval(cond, sp)(env)
while (flag) {
eval(lbody, sp)(env)
eval(cond, sp)(env)
}
eval(body, sp)(env)
}
}

@ -0,0 +1,105 @@
package project2
import java.io._
import scala.io._
trait CodeGenerator {
// Define the PrintWriter used to emit
// the code.
val out = new ByteArrayOutputStream
val pOut = new PrintWriter(out, true)
def stream = pOut
def emitCode(ast: Language.Exp): Unit
// Emit the code and return the String
// representation of the code
def code(ast: Language.Exp) = {
emitCode(ast)
out.toString.stripLineEnd
}
}
object Runner {
import Language._
def printUsage: Unit = {
println("""Usage: run PROG [OPTION]
or: run FILE [OPTION]
OPTION: compX86, intStack, intValue (default)""")
}
def main(args: Array[String]): Unit = {
if (args.size == 0) {
printUsage
return
}
val src = if (new File(args(0)).exists) {
val source = Source.fromFile(args(0))
try source.getLines mkString "\n" finally source.close()
} else {
args(0)
}
println("============ SRC CODE ============")
println(src)
println("==================================\n")
val reader = new BaseReader(src, '\u0000')
val scanner = new Scanner(reader)
// Parser to test!
// TODO: Change this as you finish parsers
val parser = new ArithParser(scanner)
val ast = parser.parseCode
println("============= AST ================")
println(s"\t$ast")
println("==================================\n")
val analyzer = new SemanticAnalyzer(parser)
println("======= Semantic Analyzer ========")
val (numWarning, numError) = analyzer.run(ast)
println("==================================\n")
if (numError > 0)
return
val interpreter = if (args.contains("intStack"))
new StackInterpreter
else
new ValueInterpreter
println("========== Interpreter ===========")
println(s"Result ${interpreter.run(ast)}")
println("==================================\n")
// Generator to test
if (args.contains("compX86")) {
val generator = new X86Compiler with CodeGenerator
val code = generator.code(ast)
val data = Map[Char,Int]()
val runner = new ASMRunner(code, data)
println("============ OUTPUT ==============")
println(runner.code)
println("==================================\n")
if (runner.assemble != 0) {
println("Compilation error!")
} else {
println("============ RESULT ==============")
println(s"Result ${runner.run}")
println("==================================")
}
} else {
val generator = new StackCompiler with CodeGenerator
val code = generator.code(ast)
println("============ OUTPUT ==============")
println(code)
println("==================================")
}
}
}

@ -0,0 +1,601 @@
package project2
// Class used to carry position information within the source code
case class Position(gapLine: Int, gapCol: Int, startLine: Int, startCol: Int, endLine: Int, endCol: Int)
object Tokens {
abstract class Token {
var pos: Position = _
}
case object EOF extends Token
case class Number(x: Int) extends Token
case class Ident(x: String) extends Token
case class Keyword(x: String) extends Token
case class Delim(x: Char) extends Token
}
// Scanner
class Scanner(in: Reader[Char]) extends Reader[Tokens.Token] with Reporter {
import Tokens._
// Position handling
def pos = in.pos
def input = in.input
// Current line in the file
var line = 0
// lineStarts(i) contains the offset of the i th line within the file
val lineStarts = scala.collection.mutable.ArrayBuffer(0)
// Current column in the file
def column = pos - lineStarts(line)
// Extract the i th line of code.
def getLine(i: Int) = {
val start = lineStarts(i)
val end = input.indexOf('\n', start)
if (end < 0)
input.substring(start)
else
input.substring(start, end)
}
// Information for the current Position
var gapLine = 0;
var gapCol = 0;
var startLine = 0;
var startCol = 0;
var endLine = 0;
var endCol = 0;
override def abort(msg: String) = {
abort(msg, showSource(getCurrentPos()))
}
/*
* Show the line of code and highlight the token at position p
*/
def showSource(p: Position) = {
val width = if (p.endLine == p.startLine) (p.endCol - p.startCol) else 0
val header = s"${p.startLine + 1}:${p.startCol + 1}: "
val line1 = getLine(p.startLine)
val line2 = " "*(p.startCol+header.length) + "^"*(width max 1)
header + line1 + '\n' + line2
}
def isAlpha(c: Char) =
('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
def isDigit(c: Char) = '0' <= c && c <= '9'
def isAlphaNum(c: Char) = isAlpha(c) || isDigit(c)
def isCommentStart(c1: Char, c2: Char) = c1 == '/' && c2 == '/'
val isWhiteSpace = Set(' ','\t','\n','\r')
// Boolean operators start with one of the following characters
val isBOperator = Set('<', '>', '!', '=')
// Operators start with one of the following characters
val isOperator = Set('+','-','*','/') ++ isBOperator
// List of delimiters
// TODO: Update this as delimiters are added to our language
val isDelim = Set('(',')')
// List of keywords
// TODO: Update this as keywords are added to our language
val isKeyword = Set[String]("val", "var")
/*
* Extract a name from the stream
*/
def getName() = {
val buf = new StringBuilder
while (in.hasNext(isAlphaNum)) {
buf += in.next()
}
val s = buf.toString
if (isKeyword(s)) Keyword(s) else Ident(s)
}
/*
* Extract an operator from the stream
*/
def getOperator() = {
val buf = new StringBuilder
do {
buf += in.next()
} while (in.hasNext(isOperator))
val s = buf.toString
// "=" is a delimiter, "=>" is a keyword, "==","=+", etc are operators
if (s == "=") Delim('=')
else if (isKeyword(s)) Keyword(s) else Ident(s)
}
/*
* Extract a number from the stream and return it.
* Raise an error if there is overflow.
*
* NOTE: An integer can be between 0 and (2 to the power 31) minus 1
* TODO: implement the method
*/
def getNum() = {
val buf = new java.lang.StringBuilder
while (in.hasNext(isNum)) {
buf += in.next
}
Number(buf.toString().toInt)
}
/*
* Extract a raw token from the stream.
* i.e. without position information.
*/
def getRawToken(): Token = {
if (in.hasNext(isAlpha)) {
getName()
} else if (in.hasNext(isOperator)) {
getOperator()
} else if (in.hasNext(isDigit)) {
getNum()
} else if (in.hasNext(isDelim)) {
Delim(in.next())
} else if (!in.hasNext) {
EOF
} else {
abort(s"unexpected character")
}
}
/*
* Skip white space and comments. Stop at the next token.
*/
def skipWhiteSpace() = {
while (in.hasNext(isWhiteSpace) || in.hasNext2(isCommentStart)) {
// If it is a comment, consume the full line
if (in.peek == '/') {
in.next()
while (in.peek != '\n') in.next()
}
// Update file statistics if new line
if (in.peek == '\n') {
lineStarts += pos + 1
line += 1
}
in.next()
}
}
def getCurrentPos() = {
endLine = line; endCol = column
Position(gapLine,gapCol,startLine,startCol,endLine,endCol)
}
/*
* Extract a token and set position information
*/
def getToken(): Token = {
gapLine = line; gapCol = column
skipWhiteSpace()
startLine = line; startCol = column
val tok = getRawToken()
tok.pos = getCurrentPos()
tok
}
var peek = getToken()
var peek1 = getToken()
def hasNext: Boolean = peek != EOF
def hasNext(f: Token => Boolean) = f(peek)
def hasNext2(f: (Token, Token) => Boolean) = f(peek, peek1)
def next() = {
val res = peek
peek = peek1
peek1 = getToken()
res
}
}
class Parser(in: Scanner) extends Reporter {
import Tokens._
/*
* Overloaded methods that show the source code
* and highlight the current token when reporting
* an error.
*/
override def expected(msg: String) = {
expected(msg, in.showSource(in.peek.pos))
}
override def abort(msg: String) = {
abort(msg, in.showSource(in.peek.pos))
}
def error(msg: String, pos: Position): Unit =
error(msg, in.showSource(pos))
def warn(msg: String, pos: Position): Unit =
warn(msg, in.showSource(pos))
def accept(c: Char) = {
if (in.hasNext(_ == Delim(c))) in.next()
else expected(s"'$c'")
}
def accept(s: String) = {
if (in.hasNext(_ == Keyword(s))) in.next()
else expected(s"'$s'")
}
/*
* Auxilaries functions
* Test and extract data
*/
def isName(x: Token) = x match {
case Ident(x) => true
case _ => false
}
def getName(): (String, Position) = {
if (!in.hasNext(isName)) expected("Name")
val pos = in.peek.pos
val Ident(x) = in.next()
(x, pos)
}
def isNum(x: Token) = x match {
case Number(x) => true
case _ => false
}
def getNum(): (Int, Position) = {
if (!in.hasNext(isNum)) expected("Number")
val pos = in.peek.pos
val Number(x) = in.next()
(x, pos)
}
def getOperator(): (String, Position) = {
if (!in.hasNext(isName)) expected("Operator")
val pos = in.peek.pos
val Ident(x) = in.next()
(x, pos)
}
/*
* Test if the following token is an infix
* operator with highest precedence
*/
def isInfixOp(min: Int)(x: Token) = x match {
case Ident(x) => prec(x) >= min
case _ => false
}
/*
* Test if the following token is an operator.
*/
def isOperator(x: Token) = x match {
case Ident(x) => in.isOperator(x.charAt(0))
case _ => false
}
/*
* Define precedence of operator.
* Negative precedence means that the operator can
* not be used as an infix operator within a simple expression.
*/
def prec(a: String) = a match { // higher bind tighter
case "+" | "-" => 1
case "*" | "/" => 2
case _ if in.isBOperator(a.charAt(0)) => -1
case _ => 0
}
def assoc(a: String) = a match {
case "+" | "-" | "*" | "/" => 1
case _ => 1
}
}
/**
* Definition of our target language.
*
* The different nodes of the AST also keep Position information
* for error handling during the semantic analysis.
*
* TODO: Every time you add an AST node, you must also track the position
*/
object Language {
abstract class Exp {
var pos: Position = _
def withPos(p: Position) = {
pos = p
this
}
}
// Arithmetic
case class Lit(x: Int) extends Exp
case class Unary(op: String, v: Exp) extends Exp
case class Prim(op: String, lop: Exp, rop: Exp) extends Exp
// Immutable variables
case class Let(x: String, a: Exp, b: Exp) extends Exp
case class Ref(x: String) extends Exp
// Branches
case class Cond(op: String, lop: Exp, rop: Exp) extends Exp
case class If(cond: Cond, tBranch: Exp, eBranch: Exp) extends Exp
// Mutable variables
case class VarDec(x: String, rhs: Exp, body: Exp) extends Exp
case class VarAssign(x: String, rhs: Exp) extends Exp
// While loops
case class While(cond: Cond, lBody: Exp, body: Exp) extends Exp
}
/*
* In the previous project, we highlighted some of the difficulties of operator precedence. We proposed a solution
* that works well, but requires some duplication of code. What happens if we add another operator such as '&'?
* What is the correct parsing for 3 & 1 + 3 * 8 & 2?
*
* 1) (3 & 1) + (3 * (8 & 2)) & has higher precedence than + and *
* 2) (3 & 1) + ((3 * 8) & 2) & has higher precedence than + but lower than *
* 3) (3 & (1 + (3 * 8))) & 2 & has lower precedence than + and * and is left associative
* 4) 3 & ((1 + (3 * 8)) & 2) & has lower precedence than + and * and is right associative
*
* In any case, it seems that we would add a new function to handle this operator. And then we must think
* about what will happen with '==',
* '~', '|' etc.!
*
* We are therefore going to implement the algorithm we have seen in class and parse the following grammar. The
* operator precedence is given through the prec function defined in Parser, and the associativity is given
* through the assoc function.
*
* <op> ::= ['*' | '/' | '+' | '-']+
* <atom> ::= <number>
* | '('<exp>')'
* <uatom> ::= [<op>]<atom>
* <exp> ::= <uatom>[<op><uatom>]*
*
* We have expanded our grammar to allow many more operators. For example "+++++++++" would be a valid syntax.
* We don't really need it for now, but it will be useful later. It also allows us to handle generic
* operators.
*
* We also introduce unary operators. These are always attached to the subsequent atom. There is no precedence
* associated with them.
*
* Our grammar may look a little bit nondeterministic now. For example, how do we parse the following code
* (Lit omitted)?
*
* 1*-4 => Prim("*-", 1, 4)
* => Prim("*", 1, Unary("-", 4))
*
* We are going to enforce the "longest match rule." When extracting the first operator in 1*-4, the longest
* match that satisfies the grammar is "*-". However, 1* -4 will be parsed as Prim("*", 1, Unary("-", 4)).
* The longest match rule has already been implemented for you in the Scanner class.
*
* The parser only enforces syntax: even if "*-" is not a valid operation, it is valid syntax. The semantic
* analysis will catch the error.
*
* TODO: complete the implementation of the parseExpression method using the algorithm we have seen in class.
*/
class ArithParser(in: Scanner) extends Parser(in) {
import Language._
import Tokens._
def parseCode = {
val res = parseExpression
if (in.hasNext)
expected(s"EOF")
res
}
def parseAtom: Exp = in.peek match {
case Delim('(') =>
in.next()
val res = parseExpression
accept(')')
res
case Number(x) =>
val (lit, pos) = getNum
Lit(lit).withPos(pos)
case _ => expected(s"Atom")
}
def parseUAtom: Exp = if (in.hasNext(isOperator)) {
val (op, pos) = getOperator
Unary(op, parseAtom).withPos(pos)
} else {
parseAtom
}
def parseExpression: Exp = parseExpression(0)
def parseExpression(min: Int): Exp = {
var res = parseUAtom
// TODO: complete
res
}
}
/*
* Now we can introduce immutable variables. An atom can now be
* a reference to a variable exactly like a number.
*
* We do not introduce the variable declaration at the
* same level as the expressions we have used thus far.
* Instead, we downgrade the arithmetic expression to a simple expression,
* as shown in the BNF below.
*
* For now we can say that it is a design choice.
*
* TODO: implement the case Ident in parseAtom
*
* <op> ::= ['*' | '/' | '+' | '-']+
* <atom> ::= <number>
* | '('<simp>')'
* | <ident>
* <uatom> ::= [<op>]<atom>
* <simp> ::= <uatom>[<op><uatom>]*
* <exp> ::= <simp>
* | 'val' <ident> '=' <simp>';' <exp>
*/
class LetParser(in: Scanner) extends ArithParser(in) {
import Language._
import Tokens._
override def parseAtom: Exp = in.peek match {
case Delim('(') =>
in.next()
val res = parseSimpleExpression
accept(')')
res
case Number(x) =>
val (_, pos) = getNum
Lit(x).withPos(pos)
case Ident(x) =>
val (_, pos) = getName
Ref(x).withPos(pos)
// TODO: remove ??? and add the implementation for this case
case _ => abort(s"Illegal start of simple expression")
}
def parseSimpleExpression = super.parseExpression
override def parseExpression = in.peek match {
case Keyword("val") =>
in.next()
val (name, pos) = getName
accept('=')
val rhs = parseSimpleExpression
accept(';')
val body = parseExpression
Let(name, rhs, body).withPos(pos)
case _ => parseSimpleExpression
}
}
/*
* We can now add if-else statements to our language. Because
* we don't yet have Boolean variables, we instead define a list
* of boolean operators which can be used. Similarly, we define
* an if-else statement as described in the BNF below:
*
* <op> ::= ['*' | '/' | '+' | '-' ]+
* <bop> ::= ('<' | '>' | '=' | '!')[<op>]
* <atom> ::= <number>
* | '('<simp>')'
* | <ident>
* | '{'<exp>'}'
* <uatom> ::= [<op>]<atom>
* <cond> ::= <simp><bop><simp>
* <simp> ::= <uatom>[<op><uatom>]*
* | 'if' '('<cond>')' <simp> 'else' <simp>
* <exp> ::= <simp>
* | 'val' <ident> '=' <simp>';' <exp>
*/
class BranchParser(in: Scanner) extends LetParser(in) {
import Language._
import Tokens._
override def parseAtom: Exp = in.peek match {
case Delim('{') =>
val pos = in.next().pos
val res = parseExpression
accept('}')
res
case _ => super.parseAtom
}
def parseCondition: Cond = {
val l = parseSimpleExpression
if (in.hasNext(isOperator)) {
val (op, pos) = getOperator
val r = parseSimpleExpression
Cond(op, l, r).withPos(pos).asInstanceOf[Cond]
} else {
expected(s"operator")
}
}
// TODO: remove ??? and complete the implementation.
override def parseSimpleExpression = ???
}
/*
* We can now introduce mutable variables using the 'var' keyword.
* When parsing statements involving mutable variables, we must
* look for declarations as well as assignments, as described
* here:
*
* <op> ::= ['*' | '/' | '+' | '-' ]+
* <bop> ::= ('<' | '>' | '=' | '!')[<op>]
* <atom> ::= <number>
* | '('<simp>')'
* | <ident>
* | '{'<exp>'}'
* <uatom> ::= [<uop>]<atom>
* <cond> ::= <simp> <bop> <simp>
* <simp> ::= <uatom>[<op><uatom>]*
* | 'if' '('<cond>')' <simp> 'else' <simp>
* | <ident> '=' <simp>
* <exp> ::= <simp>
* | 'val' <ident> '=' <simp>';' <exp>
* | 'var' <ident> '=' <simp>';' <exp>
*/
class VariableParser(in: Scanner) extends BranchParser(in) {
import Language._
import Tokens._
// TODO: remove ??? and complete the implementation.
override def parseExpression = in.peek match {
case _ => ???
}
// TODO: remove ??? and complete the implementation.
override def parseSimpleExpression = (in.peek, in.peek1) match {
case _ => ???
}
}
/*
* Finally, we must parse while loops. These function
* similar to if-else statements: they consist of a
* condition and a loop body.
*
* <op> ::= ['*' | '/' | '+' | '-' ]+
* <bop> ::= ('<' | '>' | '=' | '!')[<op>]
* <atom> ::= <number>
* | '('<simp>')'
* | <ident>
* | '{'<exp>'}'
* <uatom> ::= [<op>]<atom>
* <cond> ::= <simp> <bop> <simp>
* <simp> ::= <uatom>[<op><uatom>]*
* | 'if' '('<cond>')' <simp> 'else' <simp>
* | <ident> '=' <simp>
* <exp> ::= <simp>
* | 'val' <ident> '=' <simp>';' <exp>
* | 'var' <ident> '=' <simp>';' <exp>
* | 'while' '('<cond>')'<simp>';' <exp>
*/
class LoopParser(in: Scanner) extends VariableParser(in) {
import Language._
import Tokens._
// TODO: remove ??? and complete the implementation.
override def parseExpression = ???
}

@ -0,0 +1,157 @@
package project2
class SemanticAnalyzer(parser: Parser) extends Reporter {
import Language._
/*
* Define an empty state for the Semantic Analyser.
*
* NOTE:
* val env = new Env
*
* env("hello") is equivalent to env.apply("hello")
*/
class Env {
def apply(name: String) = false
def isVar(name: String) = false
}
/*
* Env that keeps track of variables defined.
* The map stores true if the variable is mutable,
* false otherwise.
*/
case class VarEnv(
vars: Map[String,Boolean] = Map.empty,
outer: Env = new Env) extends Env {
/*
* Return true if the variable is already defined
* in this scope
*/
def isDefined(name: String) = vars.contains(name)
/*
* Make a copy of this object and add a mutable variable 'name'
*/
def withVar(name: String): VarEnv = {
copy(vars = vars + (name -> true))
}
/*
* Make a copy of this object and add an immutable variable 'name'
*/
def withVal(name: String): VarEnv = {
copy(vars = vars + (name -> false))
}
/*
* Return true if 'name' is a mutable variable defined in this scope
* or in the outer scope.
*/
override def isVar(name: String) = vars.get(name) match {
case None => outer.isVar(name)
case Some(mut) => mut
}
/*
* Return true if 'name' is a variable defined in this scope
* or in the outer scope.
*/
override def apply(name: String): Boolean = isDefined(name) || outer(name)
}