Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
  • shchen/cs320
  • raveendr/cs320
  • mwojnaro/cs320
3 results
Show changes
Showing
with 1460 additions and 11 deletions
object Hello
Std.printString("Hello " ++ "world!")
end Hello
object HelloInt
Std.printString("What is your name?");
val name: String = Std.readString();
Std.printString("Hello " ++ name ++ "! And how old are you?");
val age: Int(32) = Std.readInt();
Std.printString(Std.intToString(age) ++ " years old then.")
end HelloInt
object Printing
Std.printInt(0); Std.printInt(-222); Std.printInt(42);
Std.printBoolean(true); Std.printBoolean(false);
Std.printString(Std.digitToString(0));
Std.printString(Std.digitToString(5));
Std.printString(Std.digitToString(9));
Std.printString(Std.intToString(0));
Std.printString(Std.intToString(-111));
Std.printString(Std.intToString(22));
Std.printString("Hello " ++ "world!");
Std.printString("" ++ "")
end Printing
object TestLists
val l: L.List = L.Cons(5, L.Cons(-5, L.Cons(-1, L.Cons(0, L.Cons(10, L.Nil())))));
Std.printString(L.toString(L.concat(L.Cons(1, L.Cons(2, L.Nil())), L.Cons(3, L.Nil()))));
Std.printInt(L.sum(l));
Std.printString(L.toString(L.mergeSort(l)))
end TestLists
# Lab 04: Type Checker
Parsing concludes the syntactical analysis of Amy programs. Having successfully constructed an abstract syntax tree for an input program, compilers typically run one or multiple phases containing checks of a more semantical nature. Virtually all high-level programming languages enjoy some form of name analysis, whose purpose is to disambiguate symbol references throughout the program. Some languages go further and perform a series of additional checks whose goal is to rule out runtime errors statically (i.e., during compilation, or in other words, without executing the program). While the exact rules for those checks vary from language to language, this part of compilation is typically summarized as "type checking". Amy, being a statically-typed language, requires both name and type analysis.
## Prelude: From Nominal to Symbolic Trees
Recall that during parsing we created (abstract syntax) trees of the *nominal* sort: Names of variables, functions and data types were simply stored as strings. However, two names used in the program could be the same, but not refer to one and the same "thing" at runtime. During name analysis we translate from nominal trees to symbolic ones, to make it clear whether two names refer to one and the same underlying entity. That is, we explicitly replace strings by fresh identifiers which will prevent us from mixing up definitions of the same name, or referring to things that have not been defined. Amy's name analyzer is provided to you as part of this lab's skeleton, but you should read the [dedicated name analyzer page](material/NameAnalysis.md) to understand how it works.
## Introduction to Type Checking
The purpose of this lab is to implement a type checker for Amy. Our type checking rules will prevent certain errors based on the kind or shape of values that the program is manipulating. For instance, we should prevent an integer from being added to a boolean value.
Type checking is the last stage of the compiler frontend. Every program that reaches the end of this stage without an error is correct (as far as the compiler is concerned), and every program that does not is wrong. After type checking we are finally ready to interpret the program or compile it to binary code!
Typing rules for Amy are presented in detail in the [Amy specification](../amy-specification/AmySpec.md). Make sure to check correct typing for all expressions and patterns.
## Implementation
The current assignment focuses on the file `TypeChecker.scala`. As usual, the skeleton and helper methods are given to you, and you will have to complete the missing parts. In particular, you will write a compiler phase that checks whether the expressions in a given program are well-typed and report errors otherwise.
To this end you will implement a simplified form of the Hindley-Milner (HM) type-inference algorithm that you'll hear about during the lectures. Note that while not advertised as a feature to users of Amy, behind the scenes we will perform type inference. It is usually straightforward to adapt an algorithm for type inference to type checking, since one can add the user-provided type annotations to the set of constraints. This is what you will do with HM in this lab.
Compared to the presentation of HM type inference in class your type checker can be simplified in another way: Since Amy does not feature higher-order functions or polymorphic data types, types in Amy are always *simple* in the sense that they are not composed of arbitrary other types. That is, a type is either a base type (one of `Int`, `Bool` and `String`) or it is an ADT, which has a proper name (e.g. `List` or `Option` from the standard library). In the latter case, all the types in the constructor of the ADT are immediately known. For instance, the standard library's `List` is really a list of integers, so we know that the `Cons` constructor takes an `Int` and another `List`.
As a result, your algorithm will never have to deal with complex constraints over type constructors (such as the function arrow `A => B`). Instead, your constraints will always be of the form `T1 = T2` where `T1` and `T2` are either *simple* types or type variables. This is most important during unification, which otherwise would have to deal with complex types separately.
Your task now is to
- complete the `genConstraints` method which will traverse a given expression and collect all the necessary typing constraints
- implement the *unification* algorithm in the function `solveConstraints`.
Familiarize yourself with the `Constraint` and `TypeVariable` data structures in `TypeChecker.scala` and then start by implementing `genConstraints`. The structure of this method will in many cases be analogous to the AST traversal for the name analyzer. Note that `genConstraints` also takes an *expected type*. For instance, in case of addition the expected type of both operands should be `Int`. For other constructs, such as pattern `match`es it is not inherently clear what should be the type of each `case` body. In this case you can create and pass a fresh type variable.
> Do not end the execution as soon as an error occurs! Instead, collect all the errors and report them at the end of the type checking phase.
### Running your frontend
Once you have a working implementation of both `genConstraints` and `solveConstraints` you can copy over your previous work on the interpreter and run the programs produced by your frontend! Don't forget that to debug your compiler's behavior you can also use the reference compiler with the `--interpret` flag and then compare the output.
You can also run using sbt using the following command:
```bash
sbt "run --type-check <path-to-file>"
```
or with the interpreter added back to your project:
```bash
sbt "run --interpret <path-to-file>"
```
### Example
#### Negative example
Consider the following program:
```scala
object Bogus
"Amy <3" || 5
end Bogus
```
The following constraints should be generated:
```scala
TypeVar(0) == BooleanType // Top-level type
BooleanType == StringType // LHS of the || operator i.e., the type of "Amy <3")
BooleanType == IntType // RHS of the || operator i.e., the type of 5
```
And these should lead to an error, as it is not possible to unify the last to constraints. The typechecker should then report an error and stop.
#### Positive example
Consider the following program:
```scala
object Correct
3 + 4 == 5
end Correct
```
The following constraints should be generated:
```scala
TypeVar(0) == BooleanType // Top-level type
TypeVar(1) == IntType // LHS of the == operator i.e., the type of 3 + 4
TypeVar(1) == IntType // RHS of the == operator i.e., the type of 5
IntType == IntType // LHS of the + operator i.e., the type of 3
IntType == IntType // RHS of the + operator i.e., the type of 4
```
And now the unification succeeds with
```scala
TypeVar(0) := BooleanType
TypeVar(1) := IntType
```
So this program typechecks successfully.
## Skeleton
As usual, you can find the skeleton for this lab in a new branch of your
group's repository. After merging it with your existing work, the
structure of your project `src` directory should be as follows:
```text
lib
└── scallion-assembly-0.6.1.jar
library
├── ...
└── ...
examples
├── ...
└── ...
src
├── amyc
│ ├── Main.scala (updated)
│ │
│ ├── analyzer (new)
│ │ ├── SymbolTable.scala
│ │ ├── NameAnalyzer.scala
│ │ └── TypeChecker.scala
│ │
│ ├── ast
│ │ ├── Identifier.scala
│ │ ├── Printer.scala
│ │ └── TreeModule.scala
│ │
│ ├── interpreter
│ │ └── Interpreter.scala
│ │
│ ├── lib
│ │ ├── scallion_3.0.6.jar
│ │ └── silex_3.0.6.jar
│ │
│ ├── parsing
│ │ ├── Parser.scala
│ │ ├── Lexer.scala
│ │ └── Tokens.scala
│ │
│ └── utils
│ ├── AmycFatalError.scala
│ ├── Context.scala
│ ├── Document.scala
│ ├── Pipeline.scala
│ ├── Position.scala
│ ├── Reporter.scala
│ └── UniqueCounter.scala
└── test
├── scala
│ └── amyc
│ └── test
│ ├── CompilerTest.scala
│ ├── LexerTests.scala
│ ├── NameAnalyzerTests.scala (new)
│ ├── ParserTests.scala
│ ├── TestSuite.scala
│ ├── TestUtils.scala
│ └── TyperTests.scala (new)
└── resources
├── analyzer (new)
│ └── ...
├── lexer
│ └── ...
└── parser
└── ...
```
## Deliverables
Deadline: **11.04.2025 23:59:59**
You should submit the following files:
- `TypeChecker.scala`: The implementation of the type checker.
File added
object L
abstract class List
case class Nil() extends List
case class Cons(h: Int(32), t: List) extends List
def isEmpty(l : List): Boolean = { l match {
case Nil() => true
case _ => false
}}
def length(l: List): Int(32) = { l match {
case Nil() => 0
case Cons(_, t) => 1 + length(t)
}}
def head(l: List): Int(32) = {
l match {
case Cons(h, _) => h
case Nil() => error("head(Nil)")
}
}
def headOption(l: List): O.Option = {
l match {
case Cons(h, _) => O.Some(h)
case Nil() => O.None()
}
}
def reverse(l: List): List = {
reverseAcc(l, Nil())
}
def reverseAcc(l: List, acc: List): List = {
l match {
case Nil() => acc
case Cons(h, t) => reverseAcc(t, Cons(h, acc))
}
}
def indexOf(l: List, i: Int(32)): Int(32) = {
l match {
case Nil() => -1
case Cons(h, t) =>
if (h == i) { 0 }
else {
val rec: Int(32) = indexOf(t, i);
if (0 <= rec) { rec + 1 }
else { -1 }
}
}
}
def range(from: Int(32), to: Int(32)): List = {
if (to < from) { Nil() }
else {
Cons(from, range(from + 1, to))
}
}
def sum(l: List): Int(32) = { l match {
case Nil() => 0
case Cons(h, t) => h + sum(t)
}}
def concat(l1: List, l2: List): List = {
l1 match {
case Nil() => l2
case Cons(h, t) => Cons(h, concat(t, l2))
}
}
def contains(l: List, elem: Int(32)): Boolean = { l match {
case Nil() =>
false
case Cons(h, t) =>
h == elem || contains(t, elem)
}}
abstract class LPair
case class LP(l1: List, l2: List) extends LPair
def merge(l1: List, l2: List): List = {
l1 match {
case Nil() => l2
case Cons(h1, t1) =>
l2 match {
case Nil() => l1
case Cons(h2, t2) =>
if (h1 <= h2) {
Cons(h1, merge(t1, l2))
} else {
Cons(h2, merge(l1, t2))
}
}
}
}
def split(l: List): LPair = {
l match {
case Cons(h1, Cons(h2, t)) =>
val rec: LPair = split(t);
rec match {
case LP(rec1, rec2) =>
LP(Cons(h1, rec1), Cons(h2, rec2))
}
case _ =>
LP(l, Nil())
}
}
def mergeSort(l: List): List = {
l match {
case Nil() => l
case Cons(h, Nil()) => l
case xs =>
split(xs) match {
case LP(l1, l2) =>
merge(mergeSort(l1), mergeSort(l2))
}
}
}
def toString(l: List): String = { l match {
case Nil() => "List()"
case more => "List(" ++ toString1(more) ++ ")"
}}
def toString1(l : List): String = { l match {
case Cons(h, Nil()) => Std.intToString(h)
case Cons(h, t) => Std.intToString(h) ++ ", " ++ toString1(t)
}}
def take(l: List, n: Int(32)): List = {
if (n <= 0) { Nil() }
else {
l match {
case Nil() => Nil()
case Cons(h, t) =>
Cons(h, take(t, n-1))
}
}
}
end L
object O
abstract class Option
case class None() extends Option
case class Some(v: Int(32)) extends Option
def isdefined(o: Option): Boolean = {
o match {
case None() => false
case _ => true
}
}
def get(o: Option): Int(32) = {
o match {
case Some(i) => i
case None() => error("get(None)")
}
}
def getOrElse(o: Option, i: Int(32)): Int(32) = {
o match {
case None() => i
case Some(oo) => oo
}
}
def orElse(o1: Option, o2: Option): Option = {
o1 match {
case Some(_) => o1
case None() => o2
}
}
def toList(o: Option): L.List = {
o match {
case Some(i) => L.Cons(i, L.Nil())
case None() => L.Nil()
}
}
end O
/** This module contains basic functionality for Amy,
* including stub implementations for some built-in functions
* (implemented in WASM or JavaScript)
*/
object Std
def printInt(i: Int(32)): Unit = {
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(32) = {
error("") // Stub implementation
}
def intToString(i: Int(32)): String = {
if (i < 0) {
"-" ++ intToString(-i)
} else {
val rem: Int(32) = i % 10;
val div: Int(32) = i / 10;
if (div == 0) { digitToString(rem) }
else { intToString(div) ++ digitToString(rem) }
}
}
def digitToString(i: Int(32)): String = {
error("") // Stub implementation
}
def booleanToString(b: Boolean): String = {
if (b) { "true" } else { "false" }
}
end Std
# Name Analysis
# Name Analysis
In the following, we will briefly discuss the purpose and implementation of the name analyzer phase in Amy. Name analysis has three goals:
* To reject programs that do not follow the Amy naming rules.
* For correct programs, to assign a unique identifier to every name. Remember that trees coming out of the parser contain plain strings wherever a name is expected. This might lead to confusion as to what each name refers to. Therefore, during name analysis, we assign a unique identifier to each name at its definition. Later in the program, every reference to that name will use the same unique identifier.
* To populate the symbol table. The symbol table contains a mapping from identifiers to all information that you could need later in the program for that identifier. For example, for each constructor, the symbol table contains an entry with the argument types, parent, and an index for this constructor.
* To reject programs that do not follow the Amy naming rules.
* For correct programs, to assign a unique identifier to every name. Remember that trees coming out of the parser contain plain strings wherever a name is expected. This might lead to confusion as to what each name refers to. Therefore, during name analysis, we assign a unique identifier to each name at its definition. Later in the program, every reference to that name will use the same unique identifier.
* To populate the symbol table. The symbol table contains a mapping from identifiers to all information that you could need later in the program for that identifier. For example, for each constructor, the symbol table contains an entry with the argument types, parent, and an index for this constructor.
After name analysis, only name-correct programs should survive, and they should contain unique identifiers that correspond to the correct symbol in the program.
You can always look at the expected output of name analysis for a given program by invoking the reference compiler with the `--printNames` option.
## The Symbol Table
## The Symbol Table
The symbol table contains information for all kinds of entities in the program. In the first half of name analysis, we discover all definitions of symbols, assign each of them a fresh identifier, and store these identifier-definition entries in the symbol table.
The `SymbolTable` API contains three kinds of methods:
* `addX` methods will add a new object to the symbol table. Among other things, these methods turn the strings found in nominal trees into the fresh `Identifier`s we will use to construct symbolic trees.
* `getX` methods which take an `Identifier` as an argument. This is what you will be using to resolve symbols you find in the program, for example, during type checking.
* `getX` methods which take two strings as arguments. These are only useful for name analysis and should not be used later: since during name analysis unique identifiers have not been assigned to everything from the start, sometimes our compiler will need to look up a definition based on its name and the name of its containing module. Of course you should not use these methods once you already have an identifier (in particular, not during type checking).
* `addX` methods will add a new object to the symbol table. Among other things, these methods turn the strings found in nominal trees into the fresh `Identifier`s we will use to construct symbolic trees.
* `getX` methods which take an `Identifier` as an argument. This is what you will be using to resolve symbols you find in the program, for example, during type checking.
* `getX` methods which take two strings as arguments. These are only useful for name analysis and should not be used later: since during name analysis unique identifiers have not been assigned to everything from the start, sometimes our compiler will need to look up a definition based on its name and the name of its containing module. Of course you should not use these methods once you already have an identifier (in particular, not during type checking).
## The different tree modules
It is time to talk in detail about the different tree modules in the `TreeModule` file. As explained earlier, our goal is to define two very similar tree modules, with the only difference being how a (qualified) name is represented: In a *nominal* tree, i.e. one coming out of the parser, names are plain strings and qualified names are pairs of strings. On the other hand, in a *symbolic* tree, both kinds of names are unique identifiers.
It is time to talk in detail about the different tree modules in the `TreeModule` file. As explained earlier, our goal is to define two very similar tree modules, with the only difference being how a (qualified) name is represented: In a *nominal* tree, i.e. one coming out of the parser, names are plain strings and qualified names are pairs of strings. On the other hand, in a *symbolic* tree, both kinds of names are unique identifiers.
To represent either kind of tree, we define a single Scala trait called `TreeModule` which defines two *abstract type fields* `Name` and `QualifiedName`. This trait also defines all types we need to represent Amy ASTs. Many of these types depend on the abstract types.
These abstract types are filled in when we instantiate the trait. Further down in the same file you can see that we define two objects `NominalTreeModule` and `SymbolicTreeModule`, which instantiate the abstract types. In addition all types within `TreeModule` are conceptually defined separately in each of the two implementations. As a result, there is a type called `NominalTreeModule.Ite` which is *different* from the type called `SymbolicTreeModule.Ite`.
## The NameAnalyzer class
## The NameAnalyzer class
The `NameAnalyzer` class implements Amy's naming rules (section 3.4 of the Amy specification). It takes a nominal program as an input and produces a symbol table and a symbolic program.
Name analysis is split into well-defined steps. The idea is the following: we first discover all definitions in the program in the correct order, i.e., modules, types, constructors, and, finally, functions. We then rewrite function bodies and expressions to refer to the newly-introduced identifiers.
......
scalaVersion := "3.5.2"
version := "1.0.0"
organization := "ch.epfl.lara"
organizationName := "LARA"
name := "calculator"
libraryDependencies ++= Seq("org.scalatest" %% "scalatest" % "3.2.10" % "test")
\ No newline at end of file
File added
sbt.version=1.10.7
/* Copyright 2020 EPFL, Lausanne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package calculator
import scallion.*
import silex.*
sealed trait Token
case class NumberToken(value: Int) extends Token
case class OperatorToken(operator: Char) extends Token
case class ParenthesisToken(isOpen: Boolean) extends Token
case object SpaceToken extends Token
case class UnknownToken(content: String) extends Token
object CalcLexer extends Lexers with CharLexers {
type Position = Unit
type Token = calculator.Token
val lexer = Lexer(
// Operators
oneOf("-+/*!")
|> { cs => OperatorToken(cs.head) },
// Parentheses
elem('(') |> ParenthesisToken(true),
elem(')') |> ParenthesisToken(false),
// Spaces
many1(whiteSpace) |> SpaceToken,
// Numbers
{
elem('0') |
nonZero ~ many(digit)
}
|> { cs => NumberToken(cs.mkString.toInt) }
) onError {
(cs, _) => UnknownToken(cs.mkString)
}
def apply(it: String): Iterator[Token] = {
val source = Source.fromString(it, NoPositioner)
val tokens = lexer(source)
tokens.filter((token: Token) => token != SpaceToken)
}
}
sealed abstract class TokenKind(text: String) {
override def toString = text
}
case object NumberClass extends TokenKind("<number>")
case class OperatorClass(op: Char) extends TokenKind(op.toString)
case class ParenthesisClass(isOpen: Boolean) extends TokenKind(if (isOpen) "(" else ")")
case object OtherClass extends TokenKind("?")
sealed abstract class Expr
case class LitExpr(value: Int) extends Expr
case class BinaryExpr(op: Char, left: Expr, right: Expr) extends Expr
case class UnaryExpr(op: Char, inner: Expr) extends Expr
object CalcParser extends Parsers {
type Token = calculator.Token
type Kind = calculator.TokenKind
import Implicits._
override def getKind(token: Token): TokenKind = token match {
case NumberToken(_) => NumberClass
case OperatorToken(c) => OperatorClass(c)
case ParenthesisToken(o) => ParenthesisClass(o)
case _ => OtherClass
}
val number: Syntax[Expr] = accept(NumberClass) {
case NumberToken(n) => LitExpr(n)
}
def binOp(char: Char): Syntax[Char] = accept(OperatorClass(char)) {
case _ => char
}
val plus = binOp('+')
val minus = binOp('-')
val times = binOp('*')
val div = binOp('/')
val fac: Syntax[Char] = accept(OperatorClass('!')) {
case _ => '!'
}
def parens(isOpen: Boolean) = elem(ParenthesisClass(isOpen))
val open = parens(true)
val close = parens(false)
lazy val expr: Syntax[Expr] = recursive {
(term ~ moreTerms).map {
case first ~ opNexts => opNexts.foldLeft(first) {
case (acc, op ~ next) => BinaryExpr(op, acc, next)
}
}
}
lazy val term: Syntax[Expr] = (factor ~ moreFactors).map {
case first ~ opNexts => opNexts.foldLeft(first) {
case (acc, op ~ next) => BinaryExpr(op, acc, next)
}
}
lazy val moreTerms: Syntax[Seq[Char ~ Expr]] = recursive {
epsilon(Seq.empty[Char ~ Expr]) |
((plus | minus) ~ term ~ moreTerms).map {
case op ~ t ~ ots => (op ~ t) +: ots
}
}
lazy val factor: Syntax[Expr] = (basic ~ fac.opt).map {
case e ~ None => e
case e ~ Some(op) => UnaryExpr(op, e)
}
lazy val moreFactors: Syntax[Seq[Char ~ Expr]] = recursive {
epsilon(Seq.empty[Char ~ Expr]) |
((times | div) ~ factor ~ moreFactors).map {
case op ~ t ~ ots => (op ~ t) +: ots
}
}
lazy val basic: Syntax[Expr] = number | open.skip ~ expr ~ close.skip
// Or, using operators...
//
// lazy val expr: Syntax[Expr] = recursive {
// operators(factor)(
// (times | div).is(LeftAssociative),
// (plus | minus).is(LeftAssociative)
// ) {
// case (l, op, r) => BinaryExpr(op, l, r)
// }
// }
//
// Then, you can get rid of term, moreTerms, and moreFactors.
def apply(tokens: Iterator[Token]): Option[Expr] = Parser(expr)(tokens).getValue
}
object Main {
def main(args: Array[String]): Unit = {
if (!CalcParser.expr.isLL1) {
CalcParser.debug(CalcParser.expr, false)
return
}
println("Welcome to the awesome calculator expression parser.")
while (true) {
print("Enter an expression: ")
val line = scala.io.StdIn.readLine()
if (line.isEmpty) {
return
}
CalcParser(CalcLexer(line)) match {
case None => println("Could not parse your line...")
case Some(parsed) => println("Syntax tree: " + parsed)
}
}
}
}
/* Copyright 2019 EPFL, Lausanne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package calculator
import org.scalatest._
import flatspec._
class Tests extends AnyFlatSpec with Inside {
"Parser" should "be LL(1)" in {
assert(CalcParser.expr.isLL1)
}
it should "be able to parse some strings" in {
val result = CalcParser(CalcLexer("1 + 3 * (5! / 7) + 42"))
assert(result.nonEmpty)
val parsed = result.get
inside(parsed) {
case BinaryExpr('+', BinaryExpr('+', one, mult), fortytwo) => {
assert(one == LitExpr(1))
assert(fortytwo == LitExpr(42))
inside(mult) {
case BinaryExpr('*', three, BinaryExpr('/', UnaryExpr('!', five), seven)) => {
assert(three == LitExpr(3))
assert(five == LitExpr(5))
assert(seven == LitExpr(7))
}
}
}
}
}
}
\ No newline at end of file
**For a brief overview of Scallion and its purpose, you can watch [this
video](https://mediaspace.epfl.ch/media/0_lypn7l0x).** What follows below is
a slightly more detailed description, and an example project you can use
to familiarize yourself with Scallion.
## Introduction to Parser Combinators
The next part of the compiler you will be working on is the parser. The
goal of the parser is to convert the sequence of tokens generated by the
lexer into an Amy *abstract syntax tree* (AST).
There are many approaches to writing parsers, such as:
- Writing the parser by hand directly in the compiler's language using
mutually recursive functions, or
- Writing the parser in a *domain specific language* (DSL) and using a
parser generator (such as Bison) to produce the parser.
Another approach, which we will be using, is *parser combinators*. The
idea behind the approach is very simple:
- Have a set of simple primitive parsers, and
- Have ways to combine them together into more and more complex
parsers. Hence the name *parser combinators*.
Usually, those primitive parsers and combinators are provided as a
library directly in the language used by the compiler. In our case, we
will be working with **Scallion**, a Scala parser combinators library
developed by *LARA*.
Parser combinators have many advantages -- the main one being easy to
write, read and maintain.
## Scallion Parser Combinators
### Documentation
In this document, we will introduce parser combinators in Scallion and
showcase how to use them. This document is not intended to be a complete
reference to Scallion. Fortunately, the library comes with a
[comprehensive
API](https://epfl-lara.github.io/scallion) which
fulfills that role. Feel free to refer to it while working on your
project!
### Playground Project
We have set up [an example project](scallion-playground) that
implements a lexer and parser for a simple expression language using
Scallion. Feel free to experiment and play with it. The project
showcases the API of Scallion and some of the more advanced combinators.
### Setup
In Scallion, parsers are defined within a trait called `Syntaxes`. This
trait takes as parameters two types:
- The type of tokens,
- The type of *token kinds*. Token kinds represent groups of tokens.
They abstract away all the details found in the actual tokens, such
as for instance positions or identifiers name. Each token has a
unique kind.
In our case, the tokens will be of type `Token` that we introduced and
used in the previous project. The token kinds will be `TokenKind`, which
we have already defined for you.
object Parser extends Pipeline[Iterator[Token], Program]
with Parsers {
type Token = myproject.Token
type Kind = myproject.TokenKind
// Indicates the kind of the various tokens.
override def getKind(token: Token): TokenKind = TokenKind.of(token)
// You parser implementation goes here.
}
The `Parsers` trait (mixed into the `Parser` object above) comes from
Scallion and provides all functions and types you will use to define
your grammar and AST translation.
### Writing Parsers
When writing a parser using parser combinators, one defines many smaller
parsers and combines them together into more and more complex parsers.
The top-level, most complex, of those parser then defines the entire
syntax for the language. In our case, that top-level parser will be
called `program`.
All those parsers are objects of the type `Syntax[A]`. The type
parameter `A` indicates the type of values produced by the parser. For
instance, a parser of type `Syntax[Int]` produces `Int`s and a parser of
type `Syntax[Expr]` produces `Expr`s. Our top-level parser has the
following signature:
lazy val program: Parser[Program] = ...
Contrary to the types of tokens and token kinds, which are fixed, the
type of values produced is a type parameter of the various `Syntax`s.
This allows your different parsers to produce different types of values.
The various parsers are stored as `val` members of the `Parser` object.
In the case of mutually dependent parsers, we use `lazy val` instead.
lazy val definition: Syntax[ClassOrFunDef] =
functionDefinition | abstractClassDefinition | caseClassDefinition
lazy val functionDefinition: Syntax[ClassOrFunDef] = ...
lazy val abstractClassDefinition: Syntax[ClassOrFunDef] = ...
lazy val caseClassDefinition: Syntax[ClassOrFunDef] = ...
### Running Parsers
Parsers of type `Syntax[A]` can be converted to objects of type
`Parser[A]`, which have an `apply` method which takes as parameter an
iterator of tokens and returns a value of type `ParseResult[A]`, which
can be one of three things:
- A `Parsed(value, rest)`, which indicates that the parser was
successful and produced the value `value`. The entirety of the input
iterator was consumed by the parser.
- An `UnexpectedToken(token, rest)`, which indicates that the parser
encountered an unexpected token `token`. The input iterator was
consumed up to the erroneous token.
- An `UnexpectedEnd(rest)`, which indicates that the end of the
iterator was reached and the parser could not finish at this point.
The input iterator was completely consumed.
In each case, the additional value `rest` is itself some sort of a
`Parser[A]`. That parser represents the parser after the successful
parse or at the point of error. This parser could be used to provide
useful error messages or even to resume parsing.
override def run(ctx: Context)(tokens: Iterator[Token]): Program = {
import ctx.reporter._
val parser = Parser(program)
parser(tokens) match {
case Parsed(result, rest) => result
case UnexpectedEnd(rest) => fatal("Unexpected end of input.")
case UnexpectedToken(token, rest) => fatal("Unexpected token: " + token)
}
}
### Parsers and Grammars
As you will see, parsers built using parser combinators will look a lot
like grammars. However, unlike grammars, parsers not only describe the
syntax of your language, but also directly specify how to turn this
syntax into a value. Also, as we will see, parser combinators have a
richer vocabulary than your usual *BNF* grammars.
Interestingly, a lot of concepts that you have seen on grammars, such as
`FIRST` sets and nullability can be straightforwardly transposed to
parsers.
#### FIRST set
In Scallion, parsers offer a `first` method which returns the set of
token kinds that are accepted as a first token.
definition.first === Set(def, abstract, case)
#### Nullability
Parsers have a `nullable` method which checks for nullability of a
parser. The method returns `Some(value)` if the parser would produce
`value` given an empty input token sequence, and `None` if the parser
would not accept the empty sequence.
### Basic Parsers
We can now finally have a look at the toolbox we have at our disposition
to build parsers, starting from the basic parsers. Each parser that you
will write, however complex, is a combination of these basic parsers.
The basic parsers play the same role as terminal symbols do in grammars.
#### Elem
The first of the basic parsers is `elem(kind)`. The function `elem`
takes argument the kind of tokens to be accepted by the parser. The
value produced by the parser is the token that was matched. For
instance, here is how to match against the *end-of-file* token.
val eof: Parser[Token] = elem(EOFKind)
#### Accept
The function `accept` is a variant of `elem` which directly applies a
transformation to the matched token when it is produced.
val identifier: Syntax[String] = accept(IdentifierKind) {
case IdentifierToken(name) => name
}
#### Epsilon
The parser `epsilon(value)` is a parser that produces the `value`
without consuming any input. It corresponds to the *𝛆* found in
grammars.
### Parser Combinators
In this section, we will see how to combine parsers together to create
more complex parsers.
#### Disjunction
The first combinator we have is disjunction, that we write, for parsers
`p1` and `p2`, simply `p1 | p2`. When both `p1` and `p2` are of type
`Syntax[A]`, the disjunction `p1 | p2` is also of type `Syntax[A]`. The
disjunction operator is associative and commutative.
Disjunction works just as you think it does. If either of the parsers
`p1` or `p2` would accept the sequence of tokens, then the disjunction
also accepts the tokens. The value produced is the one produced by
either `p1` or `p2`.
Note that `p1` and `p2` must have disjoint `first` sets. This
restriction ensures that no ambiguities can arise and that parsing can
be done efficiently.[^1] We will see later how to automatically detect
when this is not the case and how fix the issue.
#### Sequencing
The second combinator we have is sequencing. We write, for parsers `p1`
and `p2`, the sequence of `p1` and `p2` as `p1 ~ p2`. When `p1` is of
type `A` and `p2` of type `B`, their sequence is of type `A ~ B`, which
is simply a pair of an `A` and a `B`.
If the parser `p1` accepts the prefix of a sequence of tokens and `p2`
accepts the postfix, the parser `p1 ~ p2` accepts the entire sequence
and produces the pair of values produced by `p1` and `p2`.
Note that the `first` set of `p2` should be disjoint from the `first`
set of all sub-parsers in `p1` that are *nullable* and in trailing
position (available via the `followLast` method). This restriction
ensures that the combinator does not introduce ambiguities.
#### Transforming Values
The method `map` makes it possible to apply a transformation to the
values produced by a parser. Using `map` does not influence the sequence
of tokens accepted or rejected by the parser, it merely modifies the
value produced. Generally, you will use `map` on a sequence of parsers,
as in:
lazy val abstractClassDefinition: Syntax[ClassOrFunDef] =
(kw("abstract") ~ kw("class") ~ identifier).map {
case kw ~ _ ~ id => AbstractClassDef(id).setPos(kw)
}
The above parser accepts abstract class definitions in Amy syntax. It
does so by accepting the sequence of keywords `abstract` and `class`,
followed by any identifier. The method `map` is used to convert the
produced values into an `AbstractClassDef`. The position of the keyword
`abstract` is used as the position of the definition.
#### Recursive Parsers
It is highly likely that some of your parsers will require to
recursively invoke themselves. In this case, you should indicate that
the parser is recursive using the `recursive` combinator:
lazy val expr: Syntax[Expr] = recursive {
...
}
If you were to omit it, a `StackOverflow` exception would be triggered
during the initialisation of your `Parser` object.
The `recursive` combinator in itself does not change the behaviour of
the underlying parser. It is there to *tie the knot*[^2].
In practice, it is only required in very few places. In order to avoid
`StackOverflow` exceptions during initialisation, you should make sure
that all recursive parsers (stored in `lazy val`s) must not be able to
reenter themselves without going through a `recursive` combinator
somewhere along the way.
#### Other Combinators
So far, many of the combinators that we have seen, such as disjunction
and sequencing, directly correspond to constructs found in `BNF`
grammars. Some of the combinators that we will see now are more
expressive and implement useful patterns.
##### Optional parsers using opt
The combinator `opt` makes a parser optional. The value produced by the
parser is wrapped in `Some` if the parser accepts the input sequence and
in `None` otherwise.
opt(p) === p.map(Some(_)) | epsilon(None)
##### Repetitions using many and many1
The combinator `many` returns a parser that accepts any number of
repetitions of its argument parser, including 0. The variant `many1`
forces the parser to match at least once.
##### Repetitions with separators repsep and rep1sep
The combinator `repsep` returns a parser that accepts any number of
repetitions of its argument parser, separated by an other parser,
including 0. The variant `rep1sep` forces the parser to match at least
once.
The separator parser is restricted to the type `Syntax[Unit]` to ensure
that important values do not get ignored. You may use `unit()` to on a
parser to turn its value to `Unit` if you explicitly want to ignore the
values a parser produces.
##### Binary operators with operators
Scallion also contains combinators to easily build parsers for infix
binary operators, with different associativities and priority levels.
This combinator is defined in an additional trait called `Operators`,
which you should mix into `Parsers` if you want to use the combinator.
By default, it should already be mixed-in.
val times: Syntax[String] =
accept(OperatorKind("*")) {
case _ => "*"
}
...
lazy val operation: Syntax[Expr] =
operators(number)(
// Defines the different operators, by decreasing priority.
times | div is LeftAssociative,
plus | minus is LeftAssociative,
...
) {
// Defines how to apply the various operators.
case (lhs, "*", rhs) => Times(lhs, rhs).setPos(lhs)
...
}
Documentation for `operators` is [available on this
page](https://epfl-lara.github.io/scallion/scallion/Operators.html).
##### Upcasting
In Scallion, the type `Syntax[A]` is invariant with `A`, meaning that,
even when `A` is a (strict) subtype of some type `B`, we *won\'t* have
that `Syntax[A]` is a subtype of `Syntax[B]`. To upcast a `Syntax[A]` to
a syntax `Syntax[B]` (when `A` is a subtype of `B`), you should use the
`.up[B]` method.
For instance, you may need to upcast a syntax of type
`Syntax[Literal[_]]` to a `Syntax[Expr]` in your assignment. To do so,
simply use `.up[Expr]`.
### LL(1) Checking
In Scallion, non-LL(1) parsers can be written, but the result of
applying such a parser is not specified. In practice, we therefore
restrict ourselves only to LL(1) parsers. The reason behind this is that
LL(1) parsers are unambiguous and can be run in time linear in the input
size.
Writing LL(1) parsers is non-trivial. However, some of the higher-level
combinators of Scallion already alleviate part of this pain. In
addition, LL(1) violations can be detected before the parser is run.
Syntaxes have an `isLL1` method which returns `true` if the parser is
LL(1) and `false` otherwise, and so without needing to see any tokens of
input.
#### Conflict Witnesses
In case your parser is not LL(1), the method `conflicts` of the parser
will return the set of all `LL1Conflict`s. The various conflicts are:
- `NullableConflict`, which indicates that two branches of a
disjunction are nullable.
- `FirstConflict`, which indicates that the `first` set of two
branches of a disjunction are not disjoint.
- `FollowConflict`, which indicates that the `first` set of a nullable
parser is not disjoint from the `first` set of a parser that
directly follows it.
The `LL1Conflict`s objects contain fields which can help you pinpoint
the exact location of conflicts in your parser and hopefully help you
fix those.
The helper method `debug` prints a summary of the LL(1) conflicts of a
parser. We added code in the handout skeleton so that, by default, a
report is outputted in case of conflicts when you initialise your
parser.
[^1]: Scallion is not the only parser combinator library to exist, far
from it! Many of those libraries do not have this restriction. Those
libraries generally need to backtrack to try the different
alternatives when a branch fails.
[^2]: See [a good explanation of what tying the knot means in the
context of lazy
languages.](https://stackoverflow.com/questions/357956/explanation-of-tying-the-knot)
sbt.version=1.10.7
addSbtPlugin("com.lightbend.sbt" % "sbt-proguard" % "0.3.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
\ No newline at end of file
package amyc
import ast._
import utils._
import parsing._
import analyzer._
import java.io.File
object Main extends MainHelpers {
private def parseArgs(args: Array[String]): Context = {
var ctx = Context(new Reporter, Nil)
args foreach {
case "--printTokens" => ctx = ctx.copy(printTokens = true)
case "--printTrees" => ctx = ctx.copy(printTrees = true)
case "--printNames" => ctx = ctx.copy(printNames = true)
case "--interpret" => ctx = ctx.copy(interpret = true)
case "--type-check" => ctx = ctx.copy(typeCheck = true)
case "--help" => ctx = ctx.copy(help = true)
case file => ctx = ctx.copy(files = ctx.files :+ file)
}
ctx
}
def main(args: Array[String]): Unit = {
val ctx = parseArgs(args)
if (ctx.help) {
val helpMsg = {
"""Welcome to the Amy reference compiler, v.1.5
|
|Default behavior is to compile the program to WebAssembly and print the following files:
|(1) the resulting code in WebAssembly text format (.wat),
|(2) the resulting code in WebAssembly binary format (.wasm),
|(3) a wrapper JavaScript file, to be run by nodejs (.js) and
|(4) a wrapper html file publishable by a web server (.html)
|
|Options:
| --printTokens Print lexer tokens (with positions) after lexing and exit
| --printTrees Print trees after parsing and exit
| --printNames Print trees with unique namas after name analyzer and exit
| --type-check Type-check the program and print trees
| --help Print this message
""".stripMargin
}
println(helpMsg)
sys.exit(0)
}
val pipeline =
AmyLexer.andThen(
if (ctx.printTokens) DisplayTokens
else Parser.andThen(
if (ctx.printTrees) treePrinterN("Trees after parsing")
else NameAnalyzer.andThen(
if (ctx.printNames) treePrinterS("Trees after name analysis")
else TypeChecker.andThen(
treePrinterS("Trees after type checking")))))
val files = ctx.files.map(new File(_))
try {
if (files.isEmpty) {
ctx.reporter.fatal("No input files")
}
if (ctx.interpret) {
ctx.reporter.fatal("Unsupported actions for now")
}
files.find(!_.exists()).foreach { f =>
ctx.reporter.fatal(s"File not found: ${f.getName}")
}
pipeline.run(ctx)(files)
ctx.reporter.terminateIfErrors()
} catch {
case AmycFatalError(_) =>
sys.exit(1)
}
}
}
trait MainHelpers {
import SymbolicTreeModule.{Program => SP}
import NominalTreeModule.{Program => NP}
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))
}
}
}
}
\ No newline at end of file
package amyc
package analyzer
import amyc.utils._
import amyc.ast.{Identifier, NominalTreeModule => N, SymbolicTreeModule => S}
// Name analyzer for Amy
// Takes a nominal program (names are plain string, 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 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
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
// Step 2: Check name uniqueness in modules
p.modules.foreach { m =>
val names = m.defs.groupBy(_.name)
names.foreach{ case (name, defs) =>
if (defs.size > 1) {
fatal(s"Two definitions named $name in module ${m.name}", defs.head)
}
}
}
// Step 3: Discover types
for {
m <- p.modules
case N.AbstractClassDef(name) <- m.defs
} table.addType(m.name, name)
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 4: Discover type constructors
for {
m <- p.modules
case cc@N.CaseClassDef(name, fields, parent) <- m.defs
} {
val argTypes = fields map (tt => transformType(tt, m.name))
val retType = table.getType(m.name, parent).getOrElse(fatal(s"Parent class $parent not found", cc))
table.addConstructor(m.name, name, argTypes, retType)
}
// Step 5: Discover functions signatures.
for {
m <- p.modules
case N.FunDef(name, params, retType1, _) <- m.defs
} {
val argTypes = params map (p => transformType(p.tt, m.name))
val retType2 = transformType(retType1, m.name)
table.addFunction(m.name, name, argTypes, retType2)
}
// Step 6: We now know all definitions in the program.
// Reconstruct modules and analyse function bodies/ expressions
def transformDef(df: N.ClassOrFunDef, module: String): S.ClassOrFunDef = { df match {
case N.AbstractClassDef(name) =>
S.AbstractClassDef(table.getType(module, name).get)
case N.CaseClassDef(name, _, _) =>
val Some((sym, sig)): Option[(Identifier, ConstrSig)] = table.getConstructor(module, name) : @unchecked
S.CaseClassDef(
sym,
sig.argTypes map S.TypeTree.apply,
sig.parent
)
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) : @unchecked
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)
}
def transformExpr(expr: N.Expr)
(implicit module: String, names: (Map[String, Identifier], Map[String, Identifier])): S.Expr = {
val (params, locals) = names
val res = expr match {
case N.Variable(name) =>
S.Variable(
locals.getOrElse(name, // Local variables shadow parameters!
params.getOrElse(name,
fatal(s"Variable $name not found", expr))))
case N.IntLiteral(value) =>
S.IntLiteral(value)
case N.BooleanLiteral(value) =>
S.BooleanLiteral(value)
case N.StringLiteral(value) =>
S.StringLiteral(value)
case N.UnitLiteral() =>
S.UnitLiteral()
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))
case N.Not(e) =>
S.Not(transformExpr(e))
case N.Neg(e) =>
S.Neg(transformExpr(e))
case N.Call(qname, args) =>
val owner = qname.module.getOrElse(module)
val name = qname.name
val entry = table.getConstructor(owner, name).orElse(table.getFunction(owner, name))
entry match {
case None =>
fatal(s"Function or constructor $qname not found", expr)
case Some((sym, sig)) =>
if (sig.argTypes.size != args.size) {
fatal(s"Wrong number of arguments for function/constructor $qname", expr)
}
S.Call(sym, args map transformExpr)
}
case N.Sequence(e1, e2) =>
S.Sequence(transformExpr(e1), transformExpr(e2))
case N.Let(vd, value, body) =>
if (locals.contains(vd.name)) {
fatal(s"Variable redefinition: ${vd.name}", vd)
}
if (params.contains(vd.name)) {
warning(s"Local variable ${vd.name} shadows function parameter", vd)
}
val sym = Identifier.fresh(vd.name)
val tpe = transformType(vd.tt, module)
S.Let(
S.ParamDef(sym, S.TypeTree(tpe)).setPos(vd),
transformExpr(value),
transformExpr(body)(module, (params, locals + (vd.name -> sym)))
)
case N.Ite(cond, thenn, elze) =>
S.Ite(transformExpr(cond), transformExpr(thenn), transformExpr(elze))
case N.Match(scrut, cases) =>
def transformCase(cse: N.MatchCase) = {
val N.MatchCase(pat, rhs) = cse
val (newPat, moreLocals) = transformPattern(pat)
S.MatchCase(newPat, transformExpr(rhs)(module, (params, locals ++ moreLocals)).setPos(rhs)).setPos(cse)
}
def transformPattern(pat: N.Pattern): (S.Pattern, List[(String, Identifier)]) = {
val (newPat, newNames): (S.Pattern, List[(String, Identifier)]) = pat match {
case N.WildcardPattern() =>
(S.WildcardPattern(), List())
case N.IdPattern(name) =>
if (locals.contains(name)) {
fatal(s"Pattern identifier $name already defined", pat)
}
if (params.contains(name)) {
warning("Suspicious shadowing by an Id Pattern", pat)
}
table.getConstructor(module, name) match {
case Some((_, ConstrSig(Nil, _, _))) =>
warning(s"There is a nullary constructor in this module called '$name'. Did you mean '$name()'?", pat)
case _ =>
}
val sym = Identifier.fresh(name)
(S.IdPattern(sym), List(name -> sym))
case N.LiteralPattern(lit) =>
(S.LiteralPattern(transformExpr(lit).asInstanceOf[S.Literal[Any]]), List())
case N.CaseClassPattern(constr, args) =>
val (sym, sig) = table
.getConstructor(constr.module.getOrElse(module), constr.name)
.getOrElse(fatal(s"Constructor $constr not found", pat))
if (sig.argTypes.size != args.size) {
fatal(s"Wrong number of args for constructor $constr", pat)
}
val (newPatts, moreLocals0) = (args map transformPattern).unzip
val moreLocals = moreLocals0.flatten
moreLocals.groupBy(_._1).foreach { case (name, pairs) =>
if (pairs.size > 1) {
fatal(s"Multiple definitions of $name in pattern", pat)
}
}
(S.CaseClassPattern(sym, newPatts), moreLocals)
}
(newPat.setPos(pat), newNames)
}
S.Match(transformExpr(scrut), cases map transformCase)
case N.Error(msg) =>
S.Error(transformExpr(msg))
}
res.setPos(expr)
}
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)
}
}