Files
WACC_37/src/main/wacc/renamer.scala

216 lines
7.8 KiB
Scala

package wacc
import scala.collection.mutable
object renamer {
import ast._
import types._
enum IdentType {
case Func
case Var
}
private case class Scope(
current: mutable.Map[(String, IdentType), Ident],
parent: Map[(String, IdentType), Ident]
) {
/** Create a new scope with the current scope as its parent.
*
* @return
* A new scope with an empty current scope, and this scope flattened into the parent scope.
*/
def subscope: Scope =
Scope(mutable.Map.empty, Map.empty.withDefault(current.withDefault(parent)))
/** Attempt to add a new identifier to the current scope. If the identifier already exists in
* the current scope, add an error to the error list.
*
* @param semType
* The semantic type of the identifier.
* @param name
* The name of the identifier.
* @param identType
* The identifier type (function or variable).
* @param globalNames
* The global map of identifiers to semantic types - the identifier will be added to this
* map.
* @param globalNumbering
* The global map of identifier names to the number of times they have been declared - will
* used to rename this identifier, and will be incremented.
* @param errors
* The list of errors to append to.
*/
def add(semType: SemType, name: Ident, identType: IdentType)(using
globalNames: mutable.Map[Ident, SemType],
globalNumbering: mutable.Map[String, Int],
errors: mutable.Builder[Error, List[Error]]
) = {
if (current.contains((name.v, identType))) {
errors += Error.DuplicateDeclaration(name)
} else {
val uid = globalNumbering.getOrElse(name.v, 0)
name.uid = uid
current((name.v, identType)) = name
globalNames(name) = semType
globalNumbering(name.v) = uid + 1
}
}
}
/** Check scoping of all variables and functions in the program. Also generate semantic types for
* all identifiers.
*
* @param prog
* AST of the program
* @param errors
* List of errors to append to
* @return
* Map of all (renamed) identifies to their semantic types
*/
def rename(prog: Program)(using
errors: mutable.Builder[Error, List[Error]]
): Map[Ident, SemType] =
given globalNames: mutable.Map[Ident, SemType] = mutable.Map.empty
given globalNumbering: mutable.Map[String, Int] = mutable.Map.empty
val scope = Scope(mutable.Map.empty, Map.empty)
val Program(funcs, main) = prog
funcs
// First add all function declarations to the scope
.map { case FuncDecl(retType, name, params, body) =>
val paramTypes = params.map { param =>
val paramType = SemType(param.paramType)
paramType
}
scope.add(KnownType.Func(SemType(retType), paramTypes), name, IdentType.Func)
(params zip paramTypes, body)
}
// Only then rename the function bodies
// (functions can call one-another regardless of order of declaration)
.foreach { case (params, body) =>
val functionScope = scope.subscope
params.foreach { case (param, paramType) =>
functionScope.add(paramType, param.name, IdentType.Var)
}
body.toList.foreach(rename(functionScope.subscope)) // body can shadow function params
}
main.toList.foreach(rename(scope))
globalNames.toMap
/** Check scoping of all identifies in a given AST node.
*
* @param scope
* The current scope and flattened parent scope.
* @param node
* The AST node.
* @param globalNames
* The global map of identifiers to semantic types - renamed identifiers will be added to this
* map.
* @param globalNumbering
* The global map of identifier names to the number of times they have been declared - used and
* updated during identifier renaming.
* @param errors
*/
private def rename(scope: Scope)(
node: Ident | Stmt | LValue | RValue | Expr
)(using
globalNames: mutable.Map[Ident, SemType],
globalNumbering: mutable.Map[String, Int],
errors: mutable.Builder[Error, List[Error]]
): Unit = node match {
// These cases are more interesting because the involve making subscopes
// or modifying the current scope.
case VarDecl(synType, name, value) => {
// Order matters here. Variable isn't declared until after the value is evaluated.
rename(scope)(value)
// Attempt to add the new variable to the current scope.
scope.add(SemType(synType), name, IdentType.Var)
}
case If(cond, thenStmt, elseStmt) => {
rename(scope)(cond)
// then and else both have their own scopes
thenStmt.toList.foreach(rename(scope.subscope))
elseStmt.toList.foreach(rename(scope.subscope))
}
case While(cond, body) => {
rename(scope)(cond)
// while bodies have their own scopes
body.toList.foreach(rename(scope.subscope))
}
// begin-end blocks have their own scopes
case Block(body) => body.toList.foreach(rename(scope.subscope))
// These cases are simpler, mostly just recursive calls to rename()
case Assign(lhs, value) => {
// Variables may be reassigned with their value in the rhs, so order doesn't matter here.
rename(scope)(lhs)
rename(scope)(value)
}
case Read(lhs) => rename(scope)(lhs)
case Free(expr) => rename(scope)(expr)
case Return(expr) => rename(scope)(expr)
case Exit(expr) => rename(scope)(expr)
case Print(expr, _) => rename(scope)(expr)
case NewPair(fst, snd) => {
rename(scope)(fst)
rename(scope)(snd)
}
case Call(name, args) => {
renameIdent(scope, name, IdentType.Func)
args.foreach(rename(scope))
}
case Fst(elem) => rename(scope)(elem)
case Snd(elem) => rename(scope)(elem)
case ArrayLiter(elems) => elems.foreach(rename(scope))
case ArrayElem(name, indices) => {
rename(scope)(name)
indices.toList.foreach(rename(scope))
}
case Parens(expr) => rename(scope)(expr)
case op: UnaryOp => rename(scope)(op.x)
case op: BinaryOp => {
rename(scope)(op.x)
rename(scope)(op.y)
}
// Default to variables. Only `call` uses IdentType.Func.
case id: Ident => renameIdent(scope, id, IdentType.Var)
// These literals cannot contain identifies, exit immediately.
case IntLiter(_) | BoolLiter(_) | CharLiter(_) | StrLiter(_) | PairLiter() | Skip() => ()
}
/** Lookup an identifier in the current scope and rename it. If the identifier is not found, add
* an error to the error list and add it to the current scope with an unknown type.
*
* @param scope
* The current scope and flattened parent scope.
* @param ident
* The identifier to rename.
* @param identType
* The type of the identifier (function or variable).
* @param globalNames
* Used to add not-found identifiers to scope.
* @param globalNumbering
* Used to add not-found identifiers to scope.
* @param errors
* The list of errors to append to.
*/
private def renameIdent(scope: Scope, ident: Ident, identType: IdentType)(using
globalNames: mutable.Map[Ident, SemType],
globalNumbering: mutable.Map[String, Int],
errors: mutable.Builder[Error, List[Error]]
): Unit = {
// Unfortunately map defaults only work with `.apply()`, which throws an error when the key is not found.
// Neither is there a way to check whether a default exists, so we have to use a try-catch.
try {
val Ident(_, uid) = scope.current.withDefault(scope.parent)((ident.v, identType))
ident.uid = uid
} catch {
case _: NoSuchElementException =>
errors += Error.UndefinedIdentifier(ident, identType)
scope.add(?, ident, identType)
}
}
}