diff --git a/src/main/wacc/frontend/ast.scala b/src/main/wacc/frontend/ast.scala index a291070..7e15c60 100644 --- a/src/main/wacc/frontend/ast.scala +++ b/src/main/wacc/frontend/ast.scala @@ -32,7 +32,10 @@ object ast { object StrLiter extends ParserBridgePos1Atom[String, StrLiter] case class PairLiter()(val pos: Position) extends Expr6 object PairLiter extends ParserBridgePos0[PairLiter] - case class Ident(v: String, var uid: Int = -1)(val pos: Position) extends Expr6 with LValue + case class Ident(v: String, var guid: Int = -1, var ty: types.RenamerType = types.?)( + val pos: Position + ) extends Expr6 + with LValue object Ident extends ParserBridgePos1Atom[String, Ident] { def apply(v: String)(pos: Position): Ident = new Ident(v)(pos) } @@ -186,7 +189,9 @@ object ast { /* ============================ STATEMENTS ============================ */ - sealed trait Stmt + sealed trait Stmt { + val pos: Position + } case class Skip()(val pos: Position) extends Stmt object Skip extends ParserBridgePos0[Skip] case class VarDecl(varType: Type, name: Ident, value: RValue)(val pos: Position) extends Stmt diff --git a/src/main/wacc/frontend/renamer.scala b/src/main/wacc/frontend/renamer.scala index b281283..748ed81 100644 --- a/src/main/wacc/frontend/renamer.scala +++ b/src/main/wacc/frontend/renamer.scala @@ -1,6 +1,19 @@ package wacc +import java.io.File import scala.collection.mutable +import cats.effect.IO +import cats.syntax.all._ +import cats.implicits._ +import cats.data.Chain +import cats.Foldable +import cats.Functor +import cats.data.NonEmptyList +import parsley.{Failure, Success} +import cats.data.NonEmptyChain +import cats.NonEmptyParallel + +private val MAIN = "$main" object renamer { import ast._ @@ -11,209 +24,347 @@ object renamer { case Var } + private case class ScopeKey(path: String, name: String, identType: IdentType) + private case class ScopeValue(id: Ident, public: Boolean) + private class Scope( - val current: mutable.Map[(String, IdentType), Ident], - val parent: Map[(String, IdentType), Ident] + private val current: mutable.Map[ScopeKey, ScopeValue], + private val parent: Map[ScopeKey, ScopeValue], + guidStart: Int = 0, + val guidInc: Int = 1 ) { + private var guid = guidStart + private var immutable = false + + private def nextGuid(): Int = { + val id = guid + guid += guidInc + id + } + + private def verifyMutable(): Unit = { + if (immutable) throw new IllegalStateException("Cannot modify an immutable scope") + } /** Create a new scope with the current scope as its parent. + * + * To be used for single-threaded applications. * * @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))) + def withSubscope[T](f: Scope => T): T = { + val subscope = Scope(mutable.Map.empty, Map.empty.withDefault(current.withDefault(parent)), guid, guidInc) + immutable = true + val result = f(subscope) + guid = subscope.guid // Sync GUID + immutable = false + result + } + + /** Create new scopes with the current scope as its parent and GUID numbering adjusted + * correctly. + * + * This will permanently mark the current scope as immutable, for thread safety. + * + * To be used for multi-threaded applications. + * + * @return + * New scopes with an empty current scope, and this scope flattened into the parent scope. + */ + def subscopes(n: Int): Seq[Scope] = { + verifyMutable() + immutable = true + (0 until n).map { i => + Scope( + mutable.Map.empty, + Map.empty.withDefault(current.withDefault(parent)), + guid + i * guidInc, + guidInc * n + ) + } + } /** 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 ty - * The semantic type of the variable identifier, or function identifier type. * @param name * The name of the identifier. - * @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. + * @return + * An error, if one occurred. */ - def add(ty: SemType | FuncType, name: Ident)(using - globalNames: mutable.Map[Ident, SemType], - globalFuncs: mutable.Map[Ident, FuncType], - globalNumbering: mutable.Map[String, Int], - errors: mutable.Builder[Error, List[Error]] - ) = { - val identType = ty match { + def add(name: Ident, public: Boolean = false): Chain[Error] = { + verifyMutable() + val path = name.pos.file.getCanonicalPath + val identType = name.ty match { case _: SemType => IdentType.Var case _: FuncType => IdentType.Func } - current.get((name.v, identType)) match { - case Some(Ident(_, uid)) => - errors += Error.DuplicateDeclaration(name) - name.uid = uid + val key = ScopeKey(path, name.v, identType) + current.get(key) match { + case Some(ScopeValue(Ident(_, id, _), _)) => + name.guid = id + Chain.one(Error.DuplicateDeclaration(name)) case None => - val uid = globalNumbering.getOrElse(name.v, 0) - name.uid = uid - current((name.v, identType)) = name - - ty match { - case semType: SemType => - globalNames(name) = semType - case funcType: FuncType => - globalFuncs(name) = funcType - } - globalNumbering(name.v) = uid + 1 + name.guid = nextGuid() + current(key) = ScopeValue(name, public) + Chain.empty } } - private def get(name: String, identType: IdentType): Option[Ident] = + /** Attempt to add a new identifier as an alias to another to the existing scope. + * + * @param alias + * The (new) alias identifier. + * @param orig + * The (existing) original identifier. + * + * @return + * An error, if one occurred. + */ + def addAlias(alias: Ident, orig: ScopeValue, public: Boolean = false): Chain[Error] = { + verifyMutable() + val path = alias.pos.file.getCanonicalPath + val identType = alias.ty match { + case _: SemType => IdentType.Var + case _: FuncType => IdentType.Func + } + val key = ScopeKey(path, alias.v, identType) + current.get(key) match { + case Some(ScopeValue(Ident(_, id, _), _)) => + alias.guid = id + Chain.one(Error.DuplicateDeclaration(alias)) + case None => + alias.guid = nextGuid() + current(key) = ScopeValue(orig.id, public) + Chain.empty + } + } + + def get(path: String, name: String, identType: IdentType): Option[ScopeValue] = // 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 { - Some(current.withDefault(parent)((name, identType))) + Some(current.withDefault(parent)(ScopeKey(path, name, identType))) } catch { case _: NoSuchElementException => None } - def getVar(name: String): Option[Ident] = get(name, IdentType.Var) - def getFunc(name: String): Option[Ident] = get(name, IdentType.Func) + def getVar(name: Ident): Option[Ident] = get(name.pos.file.getCanonicalPath, name.v, IdentType.Var).map(_.id) + def getFunc(name: Ident): Option[Ident] = get(name.pos.file.getCanonicalPath, name.v, IdentType.Func).map(_.id) } - /** 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], Map[Ident, FuncType]) = { - given globalNames: mutable.Map[Ident, SemType] = mutable.Map.empty - given globalFuncs: mutable.Map[Ident, FuncType] = mutable.Map.empty - given globalNumbering: mutable.Map[String, Int] = mutable.Map.empty - val scope = Scope(mutable.Map.empty, Map.empty) + private def prepareGlobalScope(partialProg: PartialProgram)(using scope: Scope): IO[(FuncDecl, Chain[FuncDecl], Chain[Error])] = { + def readImportFile(file: File): IO[String] = + IO.blocking(os.read(os.Path(file.getCanonicalPath))) + + def prepareImport(contents: String, file: File)(using scope: Scope): IO[(Chain[FuncDecl], Chain[Error])] = { + parser.parse(contents) match { + case Failure(msg) => + IO.pure(Chain.empty, Chain.one(Error.SyntaxError(file, msg))) + case Success(fn) => + val partialProg = fn(file) + for { + (main, chunks, errors) <- prepareGlobalScope(partialProg) + } yield (main +: chunks, errors) + } + + } + + val PartialProgram(imports, prog) = partialProg + + // First prepare this file's functions... val Program(funcs, main) = prog - funcs - // First add all function declarations to the scope - .map { case FuncDecl(retType, name, params, body) => + val (funcChunks, funcErrors) = funcs.foldLeft((Chain.empty[FuncDecl], Chain.empty[Error])) { + case ((chunks, errors), func@FuncDecl(retType, name, params, body)) => val paramTypes = params.map { param => val paramType = SemType(param.paramType) + param.name.ty = paramType paramType } - scope.add(FuncType(SemType(retType), paramTypes), name) - (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) - } - body.toList.foreach(rename(functionScope.subscope)) // body can shadow function params - } - main.toList.foreach(rename(scope)) - (globalNames.toMap, globalFuncs.toMap) + name.ty = FuncType(SemType(retType), paramTypes) + (chunks :+ func, errors ++ scope.add(name, public = true)) + } + // ...and main body. + val mainBodyIdent = Ident(MAIN, ty = FuncType(?, Nil))(prog.pos) + val mainBodyErrors = scope.add(mainBodyIdent, public = false) + val mainBodyChunk = FuncDecl(IntType()(prog.pos), mainBodyIdent, Nil, main)(prog.pos) + + // Now handle imports + val file = prog.pos.file + val preparedImports = imports.foldLeftM[IO, (Chain[FuncDecl], Chain[Error])]((Chain.empty[FuncDecl], Chain.empty[Error])) { + case ((chunks, errors), Import(name, funcs)) => + val importFile = File(file.getParent, name.v) + for { + contents <- readImportFile(importFile) + (importChunks, importErrors) <- prepareImport(contents, importFile) + importAliasErrors = funcs.foldMap { case ImportedFunc(srcName, aliasName) => + scope.get(importFile.getCanonicalPath, srcName.v, IdentType.Func) match { + case Some(src) if src.public => scope.addAlias(aliasName, src) + case _ => Chain.one(Error.UndefinedFunction(srcName)) + } + + } + } yield (chunks ++ importChunks, errors ++ importErrors) + } + + for { + (importChunks, importErrors) <- preparedImports + allChunks = importChunks ++ funcChunks + allErrors = importErrors ++ funcErrors ++ mainBodyErrors + } yield (mainBodyChunk, allChunks, allErrors) } + /** Check scoping of all variables and flatten a program. Also generates semantic types and parses + * any imported files. + * + * @param partialProg + * AST of the program + * @return + * (flattenedProg, errors) + */ + def rename(partialProg: PartialProgram): IO[(Program, Chain[Error])] = { + given scope: Scope = Scope(mutable.Map.empty, Map.empty) + for { + (main, chunks, errors) <- prepareGlobalScope(partialProg) + toRename = (main +: chunks).toList + res = (toRename zip scope.subscopes(toRename.size)).parTraverse { case (func@FuncDecl(retType, name, params, body), subscope) => + val paramErrors = params.foldMap { param => subscope.add(param.name) } + val bodyErrors = subscope.withSubscope { s => body.foldMap(rename(s)) } + paramErrors ++ bodyErrors + } + } yield (partialProg.self, errors) + } + + // /** 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], Map[Ident, FuncType]) = { + // given globalNames: mutable.Map[Ident, SemType] = mutable.Map.empty + // given globalFuncs: mutable.Map[Ident, FuncType] = 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(FuncType(SemType(retType), paramTypes), name) + // (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) + // } + // body.toList.foreach(rename(functionScope.subscope)) // body can shadow function params + // } + // main.toList.foreach(rename(scope)) + // (globalNames.toMap, globalFuncs.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], - globalFuncs: mutable.Map[Ident, FuncType], - 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) - } - 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)) + private def rename(scope: Scope)(node: Ident | Stmt | LValue | RValue | Expr): Chain[Error] = + node match { + // These cases are more interes/globting 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. + val errors = rename(scope)(value) + // Attempt to add the new variable to the current scope. + name.ty = SemType(synType) + errors ++ scope.add(name) + } + case If(cond, thenStmt, elseStmt) => { + val condErrors = rename(scope)(cond) + // then and else both have their own scopes + val thenErrors = scope.withSubscope(s => thenStmt.foldMap(rename(s))) + val elseErrors = scope.withSubscope(s => elseStmt.foldMap(rename(s))) + condErrors ++ thenErrors ++ elseErrors + } + case While(cond, body) => { + val condErrors = rename(scope)(cond) + // while bodies have their own scopes + val bodyErrors = scope.withSubscope(s => body.foldMap(rename(s))) + condErrors ++ bodyErrors + } + // begin-end blocks have their own scopes + case Block(body) => scope.withSubscope(s => body.foldMap(rename(s))) - // 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) => { - scope.getFunc(name.v) match { - case Some(Ident(_, uid)) => name.uid = uid - case None => - errors += Error.UndefinedFunction(name) - scope.add(FuncType(?, args.map(_ => ?)), name) + // 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) } - 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 => { - scope.getVar(id.v) match { - case Some(Ident(_, uid)) => id.uid = uid - case None => - errors += Error.UndeclaredVariable(id) - scope.add(?, id) + 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) => { + val nameErrors = scope.getFunc(name) match { + case Some(Ident(_, guid, ty)) => + name.ty = ty + name.guid = guid + Chain.empty + case None => + name.ty = FuncType(?, args.map(_ => ?)) + scope.add(name) + Chain.one(Error.UndefinedFunction(name)) + } + val argsErrors = args.foldMap(rename(scope)) + nameErrors ++ argsErrors + } + case Fst(elem) => rename(scope)(elem) + case Snd(elem) => rename(scope)(elem) + case ArrayLiter(elems) => elems.foldMap(rename(scope)) + case ArrayElem(name, indices) => { + val nameErrors = rename(scope)(name) + val indicesErrors = indices.foldMap(rename(scope)) + nameErrors ++ indicesErrors + } + 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 => { + scope.getVar(id) match { + case Some(Ident(_, guid, ty)) => + id.ty = ty + id.guid = guid + Chain.empty + case None => + id.ty = ? + scope.add(id) + Chain.one(Error.UndeclaredVariable(id)) + } + } + // These literals cannot contain identifies, exit immediately. + case IntLiter(_) | BoolLiter(_) | CharLiter(_) | StrLiter(_) | PairLiter() | Skip() => Chain.empty } - // These literals cannot contain identifies, exit immediately. - case IntLiter(_) | BoolLiter(_) | CharLiter(_) | StrLiter(_) | PairLiter() | Skip() => () - } } diff --git a/src/main/wacc/frontend/types.scala b/src/main/wacc/frontend/types.scala index 549d8a1..5251396 100644 --- a/src/main/wacc/frontend/types.scala +++ b/src/main/wacc/frontend/types.scala @@ -3,7 +3,9 @@ package wacc object types { import ast._ - sealed trait SemType { + sealed trait RenamerType + + sealed trait SemType extends RenamerType { override def toString(): String = this match { case KnownType.Int => "int" case KnownType.Bool => "bool" @@ -41,5 +43,5 @@ object types { } } - case class FuncType(returnType: SemType, params: List[SemType]) + case class FuncType(returnType: SemType, params: List[SemType]) extends RenamerType }