├── ulc ├── examples.gif ├── FileReader.scala ├── grammar.md ├── Main.scala ├── examples.ulc ├── Interpreter.test.scala ├── Parser.scala ├── README.md └── Interpreter.scala ├── .gitignore ├── .scalafmt.conf ├── README.md ├── uae ├── README.md └── Evaluation.scala ├── .github ├── workflows │ ├── publish.yml │ └── ci.yml └── scripts │ └── package.sc └── License /ulc/examples.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lenguyenthanh/compilers/HEAD/ulc/examples.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bsp 2 | .metals 3 | .ammonite 4 | .npmrc 5 | .scala 6 | *.js 7 | *.js.map 8 | .scala-build 9 | *.build_artifacts.txt 10 | bin 11 | .ammonite 12 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | runner.dialect = scala3 2 | version = 3.7.15 3 | align.preset = more 4 | maxColumn = 120 5 | rewrite.rules = [AsciiSortImports] 6 | spaces.inImportCurlyBraces = true -------------------------------------------------------------------------------- /ulc/FileReader.scala: -------------------------------------------------------------------------------- 1 | //> using scala "3.3.1" 2 | 3 | package ulc 4 | 5 | import scala.io.Source 6 | import scala.util.{ Failure, Success, Try } 7 | 8 | object FileReader: 9 | def read(path: String): Either[String, String] = 10 | Try { 11 | val bufferedSource = Source.fromFile(path) 12 | val content = bufferedSource.getLines.filter(!_.isEmpty).mkString("\n") 13 | bufferedSource.close 14 | content 15 | } match { 16 | case Success(v) => Right(v) 17 | case Failure(_) => Left(s"Cannot load file $path") 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | A playground where I learn about interpreters, compilers, type theory and everything that related to those. 4 | 5 | In general, you can play around examples by using [scala-cli](https://scala-cli.virtuslab.org/). For example: 6 | 7 | ```bash 8 | # run untyped lambda calculus REPL 9 | scala-cli run ulc 10 | 11 | # run test 12 | scala-cli test ulc 13 | 14 | # setup ide 15 | scala-cli setup-ide ulc 16 | 17 | # build native package with GraalVM 18 | scala-cli package --native-image ulc -o bin/ulc -f 19 | ``` 20 | 21 | Please go to each directory of each example for more details documentation: 22 | 23 | - [Evaluation for untyped arithmetic expression](uae) 24 | - [Interpreter for untyped lambda calculus](ulc) 25 | -------------------------------------------------------------------------------- /uae/README.md: -------------------------------------------------------------------------------- 1 | # Untyped Arithmetic Expression 2 | 3 | ## Grammar 4 | ``` 5 | t ::= terms: 6 | true constant true 7 | false constant false 8 | if t then t else t conditional 9 | 0 constant zero 10 | succ t successor 11 | pred t predecessor 12 | iszero t zero test 13 | ``` 14 | 15 | ## Example 16 | 17 | ``` 18 | if false then 0 else 1 19 | > 1 20 | iszero (pred (succ 0)) 21 | > true 22 | ``` 23 | 24 | ## TODO 25 | 26 | - Lexer 27 | - Scanner 28 | - PrettyPrint 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | name: Publish for ${{ matrix.OS }} 10 | runs-on: ${{ matrix.OS }} 11 | strategy: 12 | matrix: 13 | OS: ["ubuntu-latest", "macos-latest", "windows-latest"] 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: coursier/cache-action@v6.3 17 | - uses: VirtusLab/scala-cli-setup@v0.1 18 | - name: Package app 19 | run: scala-cli .github/scripts/package.sc 20 | - name: Upload binaries to release 21 | uses: svenstaro/upload-release-action@v2 22 | with: 23 | repo_token: ${{ secrets.GITHUB_TOKEN }} 24 | file: './artifacts/**' 25 | tag: ${{ github.ref }} 26 | file_glob: true 27 | -------------------------------------------------------------------------------- /ulc/grammar.md: -------------------------------------------------------------------------------- 1 | # ulc (extended) grammar 2 | 3 | ## Syntax 4 | 5 | ### term 6 | 7 | ``` 8 | t ::= term: 9 | | x variable 10 | | λx. t abstraction 11 | | t t application 12 | | (t) grouping 13 | ``` 14 | 15 | ### statement 16 | 17 | ``` 18 | statement ::= statement: 19 | x = term assignment 20 | ``` 21 | 22 | ### comment 23 | 24 | ``` 25 | comment ::= comment: 26 | -- string single line comment 27 | ``` 28 | 29 | Note: Only support comment at start of a line. 30 | 31 | ### program 32 | 33 | ``` 34 | program ::= program: 35 | | term term 36 | | statement statement 37 | | comment comment 38 | ``` 39 | -------------------------------------------------------------------------------- /.github/scripts/package.sc: -------------------------------------------------------------------------------- 1 | //> using scala "3.3.1" 2 | //> using lib "com.lihaoyi::os-lib:0.9.1" 3 | 4 | import scala.util.Properties 5 | 6 | val platformSuffix: String = { 7 | val os = 8 | if (Properties.isWin) "pc-win32" 9 | else if (Properties.isLinux) "pc-linux" 10 | else if (Properties.isMac) "apple-darwin" 11 | else sys.error(s"Unrecognized OS: ${sys.props("os.name")}") 12 | os 13 | } 14 | val artifactsPath = os.Path("artifacts", os.pwd) 15 | 16 | def destPath(prefix: String) = 17 | if (Properties.isWin) artifactsPath / s"$prefix-ulc-$platformSuffix.exe" 18 | else artifactsPath / s"$prefix-ulc-$platformSuffix" 19 | 20 | val scalaCLILauncher = 21 | if (Properties.isWin) "scala-cli.bat" else "scala-cli" 22 | 23 | os.makeDir(artifactsPath) 24 | os.proc(scalaCLILauncher, "package", "./ulc", "-o", destPath("graal"), "--native-image") 25 | .call(cwd = os.pwd) 26 | .out 27 | .text() 28 | .trim 29 | 30 | os.proc(scalaCLILauncher, "package", "./ulc", "-o", destPath("native"), "--native", "--native-version", "0.4.14") 31 | .call(cwd = os.pwd) 32 | .out 33 | .text() 34 | .trim 35 | -------------------------------------------------------------------------------- /ulc/Main.scala: -------------------------------------------------------------------------------- 1 | //> using scala "3.3.1" 2 | 3 | package ulc 4 | 5 | def loop = 6 | import scala.io.StdIn.readLine 7 | import Parser.{ Term, Stmt } 8 | 9 | val interpreter = Interpreter() 10 | val quitCommands = List(":quit", ":q") 11 | var input = "" 12 | 13 | val welcome = """ 14 | |Welcome to ulc repl! 15 | |Enter :load or :l and then file name to load a program 16 | |Enter :quit or :q to quite the repl 17 | """.stripMargin 18 | println(welcome) 19 | while { 20 | input = readLine("λ> ") 21 | quitCommands.indexOf(input) == -1 22 | } 23 | do 24 | val line = input match 25 | case s":load $s" => 26 | load(s) 27 | case s":l $s" => 28 | load(s) 29 | case s => 30 | println(removeRedundantParent(interpreter.eval(s))) 31 | 32 | def load(path: String) = 33 | val r = for 34 | str <- FileReader.read(path) 35 | _ <- interpreter.load(str) 36 | yield () 37 | println(r.fold({ s => s"Load failed $path: $s" }, { _ => s"Loaded $path!" })) 38 | 39 | def removeRedundantParent(str: String) = 40 | str match 41 | case s"($s)" => s 42 | case _ => str 43 | 44 | @main def main() = loop 45 | -------------------------------------------------------------------------------- /ulc/examples.ulc: -------------------------------------------------------------------------------- 1 | -- Boolean values 2 | -- true 3 | T = λx. λy. x 4 | -- false 5 | F = λx. λy. y 6 | 7 | -- Boolean operators 8 | ! = λb. b F T 9 | && = λa. λb. a b F 10 | || = λa. λb. a T b 11 | 12 | -- if expression 13 | if = λb. λx. λy. b x y 14 | 15 | -- pair 16 | pair = λf. λs. λb. b f s 17 | fst = λp. p T 18 | snd = λp. p F 19 | 20 | -- numbers 21 | 0 = λs. λz. z 22 | 1 = λs. λz. s z 23 | 2 = λs. λz. s (s z) 24 | 25 | -- number operations 26 | 27 | -- successor number 28 | succ = λn. λs. λz. s (n s z) 29 | 30 | -- Plus 31 | + = λa. λb. λs. λz. a s (b s z) 32 | -- Multiply 33 | * = λm. λn. m (+ n) 0 34 | -- Power 35 | ^ = λm. λn. n (* m) 1 36 | 37 | -- Check a number is Zero or not 38 | is0 = λn. n (λx. F) T 39 | zz = pair 0 0 40 | ss = λp. pair (snd p) (+ 1 (snd p)) 41 | 42 | -- previous number 43 | pred = λm. fst (m ss zz) 44 | 45 | -- Minus 46 | - = λm. λn. n pred m 47 | 48 | -- Number equality check 49 | eqN = λm. λn. && (is0 (- m n)) (is0 (- n m)) 50 | 51 | -- Y combinator 52 | Y = λf.(λx.f(λy.(x x)y))(λx.f(λy.(x x)y)) 53 | 54 | -- Factorial 55 | fact' = λr. λn. (is0 n) 1 (* n (r (pred n))) 56 | fact = λn. Y fact' n 57 | 58 | -- Some more numbers 59 | 2 = + 1 1 60 | 3 = + 2 1 61 | 4 = * 2 2 62 | 8 = * 4 2 63 | 16 = ^ 2 4 64 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "v*" 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.OS }} 13 | strategy: 14 | matrix: 15 | OS: ["ubuntu-latest", "macos-latest", "windows-latest"] 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - uses: coursier/cache-action@v6.3 21 | - uses: VirtusLab/scala-cli-setup@v0.1 22 | - name: Test 23 | run: scala-cli test ulc --cross 24 | - name: Package app 25 | run: scala-cli .github/scripts/package.sc 26 | - uses: actions/upload-artifact@v3 27 | with: 28 | name: artifacts 29 | path: artifacts 30 | if-no-files-found: error 31 | retention-days: 2 32 | format: 33 | runs-on: "ubuntu-latest" 34 | steps: 35 | - uses: actions/checkout@v3 36 | with: 37 | fetch-depth: 0 38 | - uses: coursier/cache-action@v6.3 39 | - uses: VirtusLab/scala-cli-setup@v0.1 40 | - name: Scalafmt check 41 | run: | 42 | scala-cli fmt --check ulc || ( 43 | echo "To format code run" 44 | echo " scala-cli fmt ." 45 | exit 1 46 | ) 47 | -------------------------------------------------------------------------------- /ulc/Interpreter.test.scala: -------------------------------------------------------------------------------- 1 | //> using scala "3.3.1" 2 | //> using lib "org.typelevel::cats-core:2.10.0" 3 | //> using lib "org.scalameta::munit::1.0.0" 4 | 5 | package ulc 6 | 7 | import cats.syntax.all.* 8 | import cats.instances.all.* 9 | 10 | class UlcSuite extends munit.FunSuite: 11 | val input = List( 12 | "\\x. \\y. y", 13 | "\\x. x", 14 | "(\\x. x) x", 15 | "(\\x. x x) x", 16 | "(\\x. \\y. x) a b", 17 | "\\a. \\b. \\s. \\z. a s (b s z)", 18 | "(\\n. \\f. \\x. f (n f x)) (\\f. \\x. x)", 19 | "λf.(λx.f(λy.(x x)y))(λx.f(λy.(x x)y))" 20 | ) 21 | 22 | val stmt = List( 23 | "y = \\x. x" 24 | ) 25 | 26 | test("Lexer") { 27 | val result = input.traverse(Lexer.scan) 28 | assert(result.isRight, true) 29 | } 30 | 31 | test("Parser") { 32 | val result = input.traverse(parse) 33 | assert(result.isRight, true) 34 | } 35 | 36 | test("Parser with line") { 37 | val result = stmt.traverse(lineParse) 38 | assert(result.isRight, true) 39 | } 40 | 41 | test("Parser fails") { 42 | val input = "\\x \\y. y" 43 | val result = parse(input) 44 | assert(result.isLeft, true) 45 | } 46 | 47 | test("de bruijn") { 48 | val result = input.map(deBruijn) 49 | assert(true, true) 50 | } 51 | 52 | test("Factorial") { 53 | val interpreter = Interpreter() 54 | val input = List( 55 | "T = λx. λy. x", 56 | "F = λx. λy. y", 57 | "0 = λf. λx. x", 58 | "1 = λf. λx. f x", 59 | "2 = λf. λx. f (f x)", 60 | "4 = λf. λx. f ( f (f (f x)))", 61 | "+ = λm. λn. λf. λx. m f (n f x)", 62 | "* = λm. λn. m (+ n) 0", 63 | "6 = + 4 2", 64 | "24 = * 4 6", 65 | "is0 = λn. n (λx. F) T", 66 | "pair = λf. λs. λb. b f s", 67 | "fst = λp. p T", 68 | "snd = λp. p F", 69 | "zz = pair 0 0", 70 | "ss = λp. pair (snd p) (+ 1 (snd p))", 71 | "prd = λm. fst (m ss zz)", 72 | "Y = λf.(λx.f(λy.(x x)y))(λx.f(λy.(x x)y))", 73 | "fact' = λr. λn. (is0 n) 1 (* n (r (prd n)))", 74 | "fact = λn. Y fact\' n" 75 | ) 76 | input.foreach(interpreter.eval) 77 | val f2 = interpreter.eval("prd 2") 78 | val two = interpreter.eval("1") 79 | assertEquals(f2, two) 80 | val f4 = interpreter.eval("fact 4") 81 | val twentyFour = interpreter.eval("24") 82 | assertEquals(f2, two) 83 | } 84 | 85 | val lineParser = Parser.parse(Parser.line) 86 | val termParser = Parser.parse(Parser.app) 87 | def parse(x: String) = 88 | for 89 | ts <- Lexer.scan(x) 90 | t <- termParser(ts) 91 | yield t 92 | 93 | def lineParse(x: String) = 94 | for 95 | ts <- Lexer.scan(x) 96 | t <- lineParser(ts) 97 | yield t 98 | 99 | def deBruijn(x: String) = 100 | for 101 | ts <- Lexer.scan(x) 102 | t <- termParser(ts) 103 | br = DeBruijn.transform(Map.empty, List())(t) 104 | yield br 105 | -------------------------------------------------------------------------------- /uae/Evaluation.scala: -------------------------------------------------------------------------------- 1 | //> using scala "3.3.1" 2 | 3 | // case class Location(val line: Int, val col: Int, val offset: Int) 4 | // case class Info(start: Location, end: Location) 5 | 6 | package uae 7 | type Info = Unit 8 | 9 | enum Term(val info: Info): 10 | case TTrue(override val info: Info) extends Term(info) 11 | case TFalse(override val info: Info) extends Term(info) 12 | case TIf(override val info: Info, val condition: Term, val tTerm: Term, val fTerm: Term) extends Term(info) 13 | case TZero(override val info: Info) extends Term(info) 14 | case TSucc(override val info: Info, val term: Term) extends Term(info) 15 | case TPrev(override val info: Info, val term: Term) extends Term(info) 16 | case TIsZero(override val info: Info, val term: Term) extends Term(info) 17 | 18 | def isNumericVal: Boolean = this match 19 | case TZero(_) => true 20 | case TSucc(_, t1) => t1.isNumericVal 21 | case _ => false 22 | 23 | def isBoolVal = this match 24 | case TTrue(_) => true 25 | case TFalse(_) => true 26 | case _ => false 27 | 28 | def isVal = isBoolVal || isNumericVal 29 | 30 | object Evaluation: 31 | import Term.* 32 | def eval1(term: Term): (Term, Option[Term]) = term match 33 | case TIf(_, TTrue(_), t2, _) => (t2, None) 34 | case TIf(_, TFalse(_), _, t3) => (t3, None) 35 | case t @ TIf(_, t1, t2, t3) => (t.copy(condition = eval1(t1)._1), None) 36 | case t @ TSucc(_, t1) if t1.isNumericVal => (t, Some(t)) 37 | case t @ TSucc(_, t1) => (t.copy(term = eval1(t1)._1), None) 38 | case TPrev(fi, t @ TZero(_)) => (t, Some(t)) 39 | case TPrev(_, TSucc(_, nv1)) if nv1.isNumericVal => (nv1, Some(nv1)) 40 | case t @ TPrev(_, t1) => (t.copy(term = eval1(t1)._1), None) 41 | case TIsZero(fi, TZero(_)) => (TTrue(fi), None) 42 | case t @ TIsZero(_, t1) => (t.copy(term = eval1(t1)._1), None) 43 | case t @ TTrue(_) => (t, Some(t)) 44 | case t @ TFalse(_) => (t, Some(t)) 45 | case t @ TZero(_) => (t, Some(t)) 46 | case _ => throw RuntimeException("NoRuleApplies") 47 | 48 | def eval(term: Term): Term = eval1(term) match 49 | case (_, Some(t)) => t 50 | case (t, None) => eval(t) 51 | 52 | @main 53 | def main = 54 | import Term.* 55 | import Evaluation.* 56 | val t1 = TIf((), TTrue(()), TTrue(()), TTrue(())) 57 | val t2 = TIf((), TTrue(()), TSucc((), TSucc((), TZero(()))), TTrue(())) 58 | val t3 = TIf((), TTrue(()), TSucc((), TSucc((), TPrev((), TSucc((), TZero(()))))), TTrue(())) 59 | println(eval(t1)) 60 | println(eval(t2)) 61 | println(eval(t3)) 62 | -------------------------------------------------------------------------------- /ulc/Parser.scala: -------------------------------------------------------------------------------- 1 | //> using scala "3.3.1" 2 | //> using lib "org.typelevel::cats-core::2.10.0" 3 | 4 | package parser 5 | 6 | import cats.Functor 7 | import cats.Monad 8 | import cats.syntax.all.* 9 | import cats.instances.all.* 10 | import cats.data.NonEmptyList 11 | 12 | trait Parser[A, +B]: 13 | def parse(input: List[A]): Either[String, (List[A], B)] 14 | 15 | def |[C](that: Parser[A, C]): Parser[A, B | C] = new Parser[A, B | C] { 16 | def parse(input: List[A]) = 17 | Parser.this.parse(input) match 18 | case Left(_) => that.parse(input) 19 | case r @ Right(_, _) => r 20 | } 21 | 22 | object Parser: 23 | 24 | def unit[A, B](b: B): Parser[A, B] = new Parser[A, B] { 25 | def parse(input: List[A]) = Right(input, b) 26 | } 27 | 28 | def head[A]: Parser[A, A] = new Parser[A, A] { 29 | def parse(input: List[A]) = 30 | input match 31 | case head :: tail => Right(tail, head) 32 | case _ => Left("Empty input") 33 | } 34 | 35 | def withFilter[A](filter: A => Boolean): Parser[A, A] = new Parser[A, A] { 36 | def parse(input: List[A]): Either[String, (List[A], A)] = 37 | input match 38 | case head :: tail => 39 | if filter(head) then Right(tail, head) 40 | else Left(s"unexpected token $head") 41 | case _ => Left("Empty input") 42 | } 43 | 44 | given [A]: Functor[[x] =>> Parser[A, x]] with 45 | def map[B, C](p: Parser[A, B])(f: B => C): Parser[A, C] = 46 | new Parser[A, C] { 47 | def parse(input: List[A]) = 48 | p.parse(input).map { case (rest, a) => (rest, f(a)) } 49 | } 50 | 51 | given [A]: Monad[[x] =>> Parser[A, x]] = new Monad[[x] =>> Parser[A, x]] { 52 | 53 | def pure[B](b: B): Parser[A, B] = new Parser[A, B] { 54 | def parse(input: List[A]) = Right(input, b) 55 | } 56 | 57 | def flatMap[B, C](p: Parser[A, B])(f: B => Parser[A, C]): Parser[A, C] = new Parser[A, C] { 58 | def parse(input: List[A]) = 59 | p.parse(input).flatMap((rest, b) => f(b).parse(rest)) 60 | } 61 | 62 | // @tailrec todo do we need tailrec? is this a hack? 63 | def tailRecM[B, C](b: B)(f: B => Parser[A, Either[B, C]]): Parser[A, C] = new Parser[A, C] { 64 | def parse(input: List[A]) = 65 | f(b).parse(input) match 66 | case Left(err) => Left(err) 67 | case Right((rest, Right(c))) => Right(rest, c) 68 | case Right((rest, Left(b1))) => tailRecM(b1)(f).parse(rest) 69 | } 70 | } 71 | 72 | extension [A, B](p: Parser[A, B]) 73 | def many: Parser[A, List[B]] = many1.map(_.toList) | Parser.unit[A, List[B]](Nil) 74 | def many1: Parser[A, NonEmptyList[B]] = 75 | for 76 | first <- p 77 | rest <- p.many 78 | yield NonEmptyList(first, rest) 79 | 80 | def ~[C](that: Parser[A, C]): Parser[A, (B, C)] = 81 | for 82 | b <- p 83 | c <- that 84 | yield (b, c) 85 | 86 | def *>[C](that: Parser[A, C]): Parser[A, C] = 87 | for 88 | _ <- p 89 | c <- that 90 | yield c 91 | 92 | def <*[C](that: Parser[A, C]): Parser[A, B] = 93 | for 94 | b <- p 95 | _ <- that 96 | yield b 97 | 98 | def ? : Parser[A, Option[B]] = 99 | input => 100 | p.parse(input) match 101 | case Right(rest, b) => Right(rest, Some(b)) 102 | case Left(_) => Right(input, None) 103 | -------------------------------------------------------------------------------- /ulc/README.md: -------------------------------------------------------------------------------- 1 | # Untyped Lambda Calculus 2 | 3 | An interpreter for [untyped lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus) (ulc) with some extended grammars implemented in Scala 3. I added some extended syntax to make it easier to use (for example let binding or \ can be used instead of λ). You can check the formal grammar definition [here](./grammar.md) 4 | 5 | ## Try 6 | 7 | If you want to try it locally, the fastest way it is go to [releases](https://github.com/lenguyenthanh/compilers/releases) page and download the binary file that works for your platform. 8 | 9 | Another the way is clone this repo and use [scala-cli]() to build or run project with the instructions in [here](../README.md). 10 | 11 | ## Features 12 | 13 | - REPL 14 | - Load programs from files 15 | - [Example](./examples.ulc) with boolean, number, Y combinator and factorial. 16 | 17 | ![Repl example](./examples.gif) 18 | 19 | ## Architecture Overview 20 | 21 | The overall architecture of this interpreter looks like: 22 | ``` 23 | Lexer Parser de Bruijn indices transformation Evaluator 24 | String =====> [Token] ======> Parsed AST ==================================> nameless AST ===========> result 25 | ``` 26 | 27 | ### Lexer 28 | 29 | First we need a [Lexer](https://en.wikipedia.org/wiki/Lexical_analysis) (or Scanner, Tokenizer) to parse the program into a list of [Token](https://en.wikipedia.org/wiki/Lexical_analysis#Token). I use [Parser Combinators](https://en.wikipedia.org/wiki/Parser_combinator) technique with help of the wonderful [cats-parse](https://github.com/typelevel/cats-parse) library. 30 | 31 | ### Parser 32 | 33 | Next step is a [Parser](https://en.wikipedia.org/wiki/Parsing), which receives a list of Token and turn it into a [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) or parsed tree or AST for short. I also using Parser Combinator technique here by building a simple and stupid [Parser Combinator](./Parser.scala) by my own. 34 | 35 | Here is the parsed AST code: 36 | 37 | ```Scala 38 | case class Stmt(val info: Info, val name: String, term: Term) 39 | enum Term(val info: Info): 40 | case Var(override val info: Info, val name: String) extends Term(info) 41 | case Abs(override val info: Info, val arg: Var, val term: Term) extends Term(info) 42 | case App(override val info: Info, val t1: Term, val t2: Term) extends Term(info) 43 | ``` 44 | 45 | Note: `val info: Info` here is stored location information of each note, which can be very helpful when we need to show errors to users (which I haven't done, but I should). 46 | 47 | ### de Bruijn Index Transformation 48 | 49 | [de Bruijn index](https://en.wikipedia.org/wiki/De_Bruijn_index) or nameless representation of terms. Quote from Type and Programming Languages book: 50 | 51 | ``` 52 | De Bruijn’s idea was that we can represent terms more straightforwardly — if less readably — by making variable occurrences point directly to their binders, rather than referring to them by name. This can be accomplished by replacing named variables by natural numbers, where the number k stands for “the variable bound by the k'th enclosing λ.” For example, the ordinary term λx.x corresponds to the nameless term λ.0, while λx.λy. x (y x) corresponds to λ.λ. 1 (0 1). Nameless terms are also sometimes called de Bruijn terms, and the numeric variables in them are called de Bruijn indices. 53 | ``` 54 | 55 | Our nameless AST is presented as below: 56 | 57 | ``` 58 | enum BTerm: 59 | case BVar(val index: Int) 60 | case BAbs(val term: BTerm) 61 | case BApp(val t1: BTerm, val t2: BTerm) 62 | ``` 63 | 64 | Notice that `BTerm` and `Term` look almost the same (without name variable and location information). 65 | 66 | ### Evaluation 67 | 68 | The final step is the evaluation. I use the exact implementation that is described [in the Evaluation section here](https://crypto.stanford.edu/~blynn/lambda/). 69 | 70 | We show the final result in de Bruijn form, which is not easy to read. I can improve it later by coloring the output (A variable occurrence and its binding site are assigned the same color, so that a 228 reader no longer has to count binding sites). 71 | 72 | ## Resources 73 | 74 | ### Lambda Calculus 75 | 76 | - [A Tutorial Introduction to the Lambda Calculus](https://personal.utdallas.edu/~gupta/courses/apl/lambda.pdf) 77 | - [History of Lambda-calculus and Combinatory Logic](http://www.users.waitrose.com/~hindley/SomePapers_PDFs/2006CarHin,HistlamRp.pdf) 78 | 79 | ### Implementation 80 | 81 | - [Chapter 5,6, 7 in Type and Programming Languages book](https://www.cis.upenn.edu/~bcpierce/tapl/) 82 | - [Ben Lynn's lambda calculus website](https://crypto.stanford.edu/~blynn/lambda/) 83 | 84 | ## TODO 85 | 86 | - Coloring the output 87 | - Better error messages 88 | - Recursion scheme 89 | - Fully functional for repl/main 90 | - Location information merger seems wrong 91 | -------------------------------------------------------------------------------- /ulc/Interpreter.scala: -------------------------------------------------------------------------------- 1 | //> using scala "3.3.1" 2 | //> using lib "org.typelevel::cats-core::2.10.0" 3 | //> using lib "org.typelevel::cats-parse::0.3.10" 4 | 5 | package ulc 6 | 7 | import cats.* 8 | import cats.syntax.all.* 9 | import cats.instances.all.* 10 | import cats.data.NonEmptyList 11 | 12 | type Env = Map[String, Parser.Term] 13 | 14 | case class Location(val line: Int, val col: Int, val offset: Int) 15 | case class Info(start: Location, end: Location): 16 | def merge(other: Info): Info = 17 | copy(end = other.end) 18 | 19 | enum BTerm: 20 | case BVar(val index: Int) 21 | case BAbs(val term: BTerm) 22 | case BApp(val t1: BTerm, val t2: BTerm) 23 | 24 | override def toString(): String = 25 | this match 26 | case BVar(index) => s"#$index" 27 | case BAbs(t) => s"(λ.$t)" 28 | case BApp(t1, t2) => s"($t1 $t2)" 29 | 30 | object Lexer: 31 | import cats.parse.{ Caret, LocationMap, Numbers as N, Parser as P, Parser0 as P0, Rfc5234 as R } 32 | import Token.* 33 | 34 | enum Token(val lexeme: String, val info: Info): 35 | case LeftParen(override val info: Info) extends Token("(", info) 36 | case RightParen(override val info: Info) extends Token(")", info) 37 | case Assign(override val info: Info) extends Token("=", info) 38 | case EndOfLine(override val info: Info) extends Token("\n", info) 39 | case Lambda(override val info: Info) extends Token("\\", info) 40 | case Dot(override val info: Info) extends Token(".", info) 41 | case Identifier(override val lexeme: String, override val info: Info) extends Token(lexeme, info) 42 | 43 | val endOfLine: P[Unit] = R.cr | R.lf 44 | val whitespaces: P0[Unit] = P.until0(!R.wsp).void 45 | val location = P.caret.map(c => Location(c.line, c.col, c.offset)) 46 | 47 | val comment = (P.string("--") *> P.until0(endOfLine) *> endOfLine).void 48 | val comments = comment.rep0 49 | 50 | // token 51 | val leftParen = P.char('(').info.map(p => LeftParen(p._2)) 52 | val rightParen = P.char(')').info.map(p => RightParen(p._2)) 53 | val assign = P.char('=').info.map(p => Assign(p._2)) 54 | val eol = endOfLine.info.map(p => EndOfLine(p._2)) 55 | val lambda = (P.char('\\') | P.char('λ')).info.map(p => Lambda(p._2)) 56 | val dot = P.char('.').info.map(p => Dot(p._2)) 57 | 58 | val allow = 59 | R.alpha | N.digit | P.charIn('!', '@', '#', '$', '%', '^', '&', '+', '-', '*', '_', '?', '<', '>', '|', '\'') 60 | 61 | val identifer = allow.rep.string.info.map(p => Identifier(p._1, p._2)) 62 | 63 | val token = comments.with1 *> (leftParen | rightParen | assign | eol | lambda | dot | identifer) 64 | .surroundedBy(whitespaces) <* comments 65 | 66 | val parser = token.rep.map(_.toList) 67 | 68 | def scan(str: String): Either[String, List[Token]] = 69 | parser.parse(str) match 70 | case Right("", ls) => Right(ls) 71 | case Right(rest, ls) => 72 | val idx = str.indexOf(rest) 73 | Left(s"Partial string $rest") 74 | case Left(err) => 75 | val idx = err.failedAtOffset 76 | val lm = LocationMap(str) 77 | Left(s"Lexer failed at $idx: $err") 78 | 79 | extension [T](p: P[T]) 80 | def info: P[(T, Info)] = (location.with1 ~ p ~ location).map { case ((s, t), e) => (t, Info(s, e)) } 81 | 82 | object Parser: 83 | import scala.reflect.Typeable 84 | 85 | import parser.Parser as P 86 | import parser.Parser.given 87 | import Lexer.Token.* 88 | import Lexer.Token 89 | import Term.* 90 | 91 | case class Stmt(val info: Info, val name: String, term: Term) 92 | enum Term(val info: Info): 93 | case Var(override val info: Info, val name: String) extends Term(info) 94 | case Abs(override val info: Info, val arg: Var, val term: Term) extends Term(info) 95 | case App(override val info: Info, val t1: Term, val t2: Term) extends Term(info) 96 | 97 | override def toString(): String = this match 98 | case Var(_, name) => name 99 | case Abs(_, arg, term) => s"λ$arg. $term" 100 | case App(_, t1, t2) => s"($t1 $t2)" 101 | 102 | def test[T: Typeable](token: Token): Boolean = 103 | token match 104 | case _: T => true 105 | case _ => false 106 | 107 | def token[T: Typeable] = P.withFilter[Token](test) 108 | 109 | lazy val termWithoutApp: P[Token, Term] = lambda | group | variable 110 | 111 | lazy val variable: P[Token, Var] = 112 | token[Identifier].map(id => Var(id.info, id.lexeme)) 113 | 114 | lazy val lambda: P[Token, Term] = 115 | for 116 | l <- token[Lambda] 117 | id <- variable 118 | _ <- token[Dot] 119 | t <- app 120 | yield Abs(l.info.merge(t.info), id, t) 121 | 122 | lazy val group = 123 | for 124 | _ <- token[LeftParen] 125 | t <- app 126 | _ <- token[RightParen] 127 | yield t 128 | 129 | lazy val app: P[Token, Term] = termWithoutApp.many1.map(collapse) 130 | 131 | lazy val stmt = 132 | for 133 | v <- variable 134 | _ <- token[Assign] 135 | t <- app 136 | yield Stmt(v.info.merge(t.info), v.name, t) 137 | 138 | lazy val line: P[Token, Stmt | Term] = stmt | app 139 | 140 | lazy val program: P[Token, NonEmptyList[Stmt | Term]] = (line <* token[EndOfLine].?).many1 141 | 142 | def collapse(ts: NonEmptyList[Term]): Term = 143 | ts match 144 | case NonEmptyList(head, Nil) => head 145 | case NonEmptyList(head, x :: Nil) => App(head.info.merge(x.info), head, x) 146 | case NonEmptyList(head, x :: y :: xs) => 147 | App(mergeInfo(ts), App(head.info.merge(x.info), head, x), collapse(NonEmptyList(y, xs))) 148 | 149 | def mergeInfo(ts: NonEmptyList[Term]): Info = 150 | ts.foldLeft(ts.head.info)((a, b) => a.merge(b.info)) 151 | 152 | def parse[B](p: P[Token, B])(tokens: List[Token]): Either[String, B] = 153 | p.parse(tokens) match 154 | case Left(err) => Left(err) 155 | case Right((Nil, term)) => Right(term) 156 | case _ => Left("Partial Parse") 157 | 158 | object DeBruijn: 159 | import BTerm.* 160 | import Parser.Term.* 161 | import Parser.Term 162 | import scala.collection.mutable.ListBuffer 163 | 164 | def transform(env: Env, free: List[String]): Term => BTerm = 165 | val localFree = ListBuffer.from(free) 166 | def go(ctx: List[String]): Term => BTerm = 167 | case Var(fi, x) => 168 | val idx = ctx.indexOf(x) 169 | if idx == -1 then 170 | env.get(x) match 171 | case Some(term) => transform(env, localFree.toList)(term) 172 | case None => 173 | val freeIdx = localFree.indexOf(x) 174 | if freeIdx == -1 then 175 | localFree += x 176 | BVar(free.length - 1) 177 | else BVar(freeIdx) 178 | else BVar(idx) 179 | case Abs(fi, x, t) => 180 | BAbs(go(x.name :: ctx)(t)) 181 | case App(fi, t1, t2) => 182 | BApp(go(ctx)(t1), go(ctx)(t2)) 183 | go(Nil) 184 | 185 | object Evaluation: 186 | import BTerm.* 187 | 188 | def termShift(d: Int): BTerm => BTerm = termShift(d, 0) 189 | 190 | // the shift operator ↑ d c for term t 191 | def termShift(d: Int, c: Int): BTerm => BTerm = 192 | def onVar(v: BVar, c: Int): BVar = 193 | if v.index >= c then v.copy(index = v.index + d) 194 | else v 195 | map(onVar, c) 196 | 197 | // The substitution [j ↦ s] t of term s for numbered j in term t 198 | def termSubst(j: Int, s: BTerm): BTerm => BTerm = 199 | def onVar(v: BVar, c: Int): BTerm = 200 | if v.index == j + c then termShift(c)(s) 201 | else v 202 | map(onVar, 0) 203 | 204 | def termSubstTop(s: BTerm): BTerm => BTerm = 205 | (termSubst(0, termShift(1)(s))) andThen termShift(-1) 206 | 207 | def map(onVar: (BVar, Int) => BTerm, c: Int)(term: BTerm): BTerm = 208 | def walk(onVar: (BVar, Int) => BTerm, c: Int, t: BTerm): BTerm = 209 | t match 210 | case t @ BVar(_) => onVar(t, c) 211 | case BAbs(t1) => BAbs(walk(onVar, c + 1, t1)) 212 | case BApp(t1, t2) => BApp(walk(onVar, c, t1), walk(onVar, c, t2)) 213 | walk(onVar, c, term) 214 | 215 | def eval(env: Env, term: BTerm): BTerm = 216 | term match 217 | case BApp(t1, t2) => 218 | val r = eval(env, t1) 219 | r match 220 | case BAbs(t12) => 221 | eval(env, termSubstTop(t2)(t12)) 222 | case _ => 223 | BApp(r, t2) 224 | case _ => 225 | term 226 | 227 | def norm(env: Env, term: BTerm, count: Int): Option[BTerm] = 228 | if count > 100000 then None 229 | else 230 | eval(env, term) match 231 | case BAbs(t) => norm(env, t, count + 1).map(BAbs(_)) 232 | case BApp(t1, t2) => 233 | for 234 | t11 <- norm(env, t1, count + 1) 235 | t22 <- norm(env, t2, count + 1) 236 | yield BApp(t11, t22) 237 | case t @ _ => Some(t) 238 | 239 | def norm(term: BTerm): BTerm = 240 | norm(Map.empty, term) 241 | 242 | def norm(env: Env, term: BTerm): BTerm = 243 | val t = eval(env, term) 244 | norm(env, t, 0) match 245 | case Some(t1) => t1 246 | case None => t 247 | 248 | class Interpreter: 249 | import Parser.{ Stmt, Term } 250 | import scala.collection.mutable.Map 251 | 252 | val env = Map[String, Term]() 253 | 254 | def load(program: String): Either[String, Unit] = 255 | for 256 | ts <- Lexer.scan(program) 257 | p <- Parser.parse(Parser.program)(ts) 258 | _ = p.map(eval) 259 | yield () 260 | 261 | def eval(input: String): String = 262 | val r = for 263 | ts <- Lexer.scan(input) 264 | t <- Parser.parse(Parser.line)(ts) 265 | yield t 266 | r match 267 | case Left(str) => 268 | s"Parse Error: $str" 269 | case Right(line) => 270 | eval(line) 271 | 272 | def eval(line: Term | Stmt): String = 273 | line match 274 | case t: Term => 275 | val bTerm = DeBruijn.transform(env.toMap, Nil)(t) 276 | Evaluation.norm(env.toMap, bTerm).toString 277 | case Stmt(_, name, term) => 278 | env += (name -> term) 279 | s"$name = $term" 280 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------