├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .scalafmt.conf ├── INTERPRETER.scala ├── LSP.scala ├── Makefile ├── README.md ├── REPL.scala ├── compiler.scala ├── docs ├── interpreter.png ├── lsp.gif └── repl.gif ├── evaluator.scala ├── examples ├── test.qmf └── test2.qmf ├── parser.scala └── project.scala /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | strategy: 7 | fail-fast: false 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-java@v3 12 | with: 13 | distribution: 'temurin' 14 | java-version: '17' 15 | 16 | - uses: VirtusLab/scala-cli-setup@main 17 | 18 | - name: CI 19 | run: make all 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .bsp 3 | .metals 4 | .scala-build 5 | .vscode 6 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.5.8" 2 | runner.dialect = scala3 3 | rewrite.scala3.insertEndMarkerMinLines = 10 4 | rewrite.scala3.removeOptionalBraces = true 5 | rewrite.scala3.convertToNewSyntax = true 6 | align.preset = more 7 | 8 | fileOverride { 9 | "glob:**.sbt" { 10 | runner.dialect = scala212source3 11 | } 12 | 13 | "glob:**/project/**.*" { 14 | runner.dialect = scala212source3 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /INTERPRETER.scala: -------------------------------------------------------------------------------- 1 | import cats.effect.* 2 | import cats.effect.std.* 3 | import fs2.io.file.* 4 | import cats.parse.* 5 | 6 | object INTERPRETER extends IOApp: 7 | def run(args: List[String]) = 8 | Files[IO] 9 | .readAll(Path(args.head)) 10 | .through(fs2.text.utf8.decode) 11 | .compile 12 | .string 13 | .flatMap { content => 14 | QuickmaffsParser.parse(content) match 15 | case Left(err) => 16 | printErrors(content, List(err.caret -> "Parsing error")) 17 | case Right(prg) => 18 | QuickmaffsCompiler.compile(prg) match 19 | case Left(errs) => 20 | printErrors( 21 | content, 22 | errs.map(ce => ce.position.from -> ce.message) 23 | ) 24 | case Right(idx) => 25 | QuickmaffsEvaluator.evaluate(prg) match 26 | case Left(errs) => 27 | printErrors( 28 | content, 29 | List(Caret(0, 0, 0) -> errs.message) 30 | ) 31 | case Right(st) => 32 | printErrors( 33 | content, 34 | List.empty, 35 | st.map { case (name, value) => 36 | idx.variables(name).definedAt.from.line -> value 37 | } 38 | ) 39 | 40 | } 41 | .as(ExitCode.Success) 42 | end INTERPRETER 43 | 44 | def printErrors( 45 | program: String, 46 | errors: Seq[(Caret, String)], 47 | results: Map[Int, Int] = Map.empty 48 | ): IO[Unit] = 49 | val byLine = errors.groupBy(_._1.line) 50 | val sb = StringBuilder() 51 | 52 | import Colors.* 53 | 54 | val size = program.linesIterator.length 55 | val round = math.log10(size).toInt + 1 56 | 57 | program.linesIterator.zipWithIndex.foreach { case (l, i) => 58 | val lineNumber = i.toString.reverse.padTo(round, '0').reverse 59 | val result = results.get(i).map(v => green(s" // $v")).getOrElse("") 60 | val pref = s"[$lineNumber]: " 61 | sb.append(yellow(pref) + l + s"$result\n") 62 | inline def shift(c: Caret) = " " * (pref.length + c.col) 63 | byLine.get(i).foreach { errs => 64 | errors.headOption.foreach { (caret, msg) => 65 | sb.append(shift(caret) + red("^") + "\n") 66 | sb.append(shift(caret) + red("|") + "\n") 67 | sb.append(shift(caret) + red(msg) + "\n") 68 | } 69 | } 70 | } 71 | 72 | IO.println(sb.result()) 73 | end printErrors 74 | -------------------------------------------------------------------------------- /LSP.scala: -------------------------------------------------------------------------------- 1 | import langoustine.lsp.* 2 | 3 | import cats.effect.* 4 | import jsonrpclib.fs2.* 5 | 6 | import fs2.io.file.Files 7 | import fs2.text 8 | import fs2.io.file.Path 9 | import cats.parse.Parser 10 | import QuickmaffsCompiler.CompileError 11 | import QuickmaffsCompiler.Index 12 | import langoustine.lsp.runtime.DocumentUri 13 | import langoustine.lsp.runtime.Opt 14 | import langoustine.lsp.runtime.uinteger 15 | 16 | import cats.syntax.all.* 17 | import langoustine.lsp.app.LangoustineApp 18 | import langoustine.lsp.tools.SemanticTokensEncoder 19 | import langoustine.lsp.tools.SemanticToken 20 | import cats.effect.std.Semaphore 21 | 22 | object LSP extends LangoustineApp: 23 | import QuickmaffsLSP.{server, State} 24 | 25 | def server( 26 | args: List[String] 27 | ): Resource[cats.effect.IO, LSPBuilder[cats.effect.IO]] = 28 | Resource 29 | .eval(IO.ref(Map.empty[DocumentUri, State])) 30 | .map { state => 31 | QuickmaffsLSP.server(state) 32 | } 33 | .onFinalize(IO.consoleForIO.errorln("Terminating server")) 34 | 35 | end LSP 36 | 37 | object QuickmaffsLSP: 38 | import requests.* 39 | import aliases.* 40 | import enumerations.* 41 | import structures.* 42 | 43 | enum State: 44 | case Empty 45 | case InvalidCode(err: QuickmaffsParser.ParsingError) 46 | case InvalidProgram(errors: Vector[CompileError]) 47 | case RuntimeError(error: QuickmaffsEvaluator.EvaluationError) 48 | case Ok( 49 | idx: Index, 50 | interpreted: Map[String, Int], 51 | program: Program[WithSpan] 52 | ) 53 | end State 54 | 55 | extension (s: cats.parse.Caret) 56 | def toPosition: Position = 57 | Position(line = s.line, character = s.col) 58 | 59 | extension (s: Span) 60 | def toRange: Range = Range(s.from.toPosition, s.to.toPosition) 61 | 62 | extension (s: Position) 63 | def toCaret = cats.parse.Caret(s.line.value, s.character.value, -1) 64 | 65 | def server(state: Ref[IO, Map[DocumentUri, State]]) = 66 | import QuickmaffsCompiler.* 67 | 68 | def process(s: String) = 69 | QuickmaffsParser.parse(s) match 70 | case Left(e) => State.InvalidCode(e) 71 | case Right(parsed) => 72 | compile(parsed) match 73 | case Left(errs) => State.InvalidProgram(errs) 74 | case Right(idx) => 75 | QuickmaffsEvaluator.evaluate(parsed) match 76 | case Left(err) => State.RuntimeError(err) 77 | case Right(ok) => State.Ok(idx, ok, parsed) 78 | end process 79 | 80 | def processFile(path: Path) = 81 | Files[IO].readAll(path).through(text.utf8.decode).compile.string.map { 82 | contents => 83 | process(contents) 84 | } 85 | 86 | def processUri(uri: DocumentUri) = 87 | val path = uri.value.drop("file://".length) 88 | processFile(Path(path)) 89 | 90 | def set(u: DocumentUri)(st: State) = 91 | state.update(_.updated(u, st)) <* IO.consoleForIO.errorln( 92 | s"State update: $u is set to ${st.getClass}" 93 | ) 94 | 95 | def get(u: DocumentUri) = 96 | state.get.map(_.get(u)) 97 | 98 | def recompile(uri: DocumentUri, back: Communicate[IO]) = 99 | def publish(vec: Vector[Diagnostic]) = 100 | back.notification( 101 | textDocument.publishDiagnostics, 102 | PublishDiagnosticsParams(uri, diagnostics = vec) 103 | ) 104 | 105 | processUri(uri) 106 | .flatTap(set(uri)) 107 | .flatTap { 108 | case _: State.Ok | State.Empty => publish(Vector.empty) 109 | case State.InvalidProgram(errs) => 110 | val diags = errs.map { case CompileError(span, msg) => 111 | Diagnostic( 112 | range = span.toRange, 113 | message = msg, 114 | severity = Opt(DiagnosticSeverity.Error) 115 | ) 116 | } 117 | 118 | publish(diags) 119 | 120 | case State.InvalidCode(parseError) => 121 | publish( 122 | Vector( 123 | Diagnostic( 124 | range = Span(parseError.caret, parseError.caret).toRange, 125 | message = "Parsing failed", 126 | severity = Opt(DiagnosticSeverity.Error) 127 | ) 128 | ) 129 | ) 130 | case State.RuntimeError(err) => 131 | val zero = 0 132 | publish( 133 | Vector( 134 | Diagnostic( 135 | range = Range(Position(zero, zero), Position(zero, zero)), 136 | message = s"Runtime: ${err.message}", 137 | severity = Opt(DiagnosticSeverity.Error) 138 | ) 139 | ) 140 | ) 141 | } 142 | .void 143 | end recompile 144 | 145 | def variableUnderCursor(doc: DocumentUri, position: Position) = 146 | get(doc).map { 147 | case Some(State.Ok(idx, _, _)) => 148 | idx.variables 149 | .find { case (name, vdf) => 150 | vdf.references.exists(_.contains(position.toCaret)) 151 | } 152 | case _ => None 153 | } 154 | 155 | val encoder = SemanticTokensEncoder( 156 | tokenTypes = Vector( 157 | SemanticTokenTypes.variable, 158 | SemanticTokenTypes.number, 159 | SemanticTokenTypes.operator 160 | ), 161 | modifiers = Vector.empty 162 | ) 163 | 164 | LSPBuilder 165 | .create[IO] 166 | .handleRequest(initialize) { (in, back) => 167 | back.notification( 168 | window.showMessage, 169 | ShowMessageParams( 170 | message = "Hello from Quickmaffs", 171 | `type` = enumerations.MessageType.Info 172 | ) 173 | ) *> 174 | IO { 175 | InitializeResult( 176 | ServerCapabilities( 177 | hoverProvider = Opt(true), 178 | definitionProvider = Opt(true), 179 | documentSymbolProvider = Opt(true), 180 | renameProvider = Opt(true), 181 | semanticTokensProvider = Opt( 182 | SemanticTokensOptions( 183 | legend = encoder.legend, 184 | full = Opt(true) 185 | ) 186 | ), 187 | textDocumentSync = Opt( 188 | TextDocumentSyncOptions( 189 | openClose = Opt(true), 190 | save = Opt(true) 191 | ) 192 | ) 193 | ), 194 | Opt( 195 | InitializeResult 196 | .ServerInfo(name = "Quickmaffs LSP", version = Opt("0.0.1")) 197 | ) 198 | ) 199 | } 200 | } 201 | .handleNotification(textDocument.didOpen) { (in, back) => 202 | recompile(in.textDocument.uri, back) 203 | } 204 | .handleNotification(textDocument.didSave) { (in, back) => 205 | recompile(in.textDocument.uri, back) 206 | } 207 | .handleRequest(textDocument.semanticTokens.full) { (in, back) => 208 | get(in.textDocument.uri).flatMap { 209 | case Some(State.Ok(idx, values, program)) => 210 | val tokens = Vector.newBuilder[SemanticToken] 211 | program.statements.map(_.value).foreach { st => 212 | st match 213 | case Statement.Ass(name, e) => 214 | inline def nameToken(tok: Expr.Name[WithSpan]) = 215 | tokenFromSpan(tok.value.span, SemanticTokenTypes.variable) 216 | 217 | inline def tokenFromSpan( 218 | span: Span, 219 | tpe: SemanticTokenTypes 220 | ) = 221 | SemanticToken.fromRange( 222 | span.toRange, 223 | tokenType = tpe 224 | ) 225 | 226 | tokens += nameToken(name) 227 | 228 | def go(expr: Expr[WithSpan]): Unit = 229 | expr match 230 | case Expr.Add(l, r, operator) => 231 | go(l) 232 | go(r) 233 | tokens += tokenFromSpan( 234 | operator.span, 235 | SemanticTokenTypes.operator 236 | ) 237 | 238 | case Expr.Mul(l, r, operator) => 239 | go(l) 240 | go(r) 241 | tokens += tokenFromSpan( 242 | operator.span, 243 | SemanticTokenTypes.operator 244 | ) 245 | 246 | case n @ Expr.Name(_) => 247 | tokens += nameToken(n) 248 | 249 | case Expr.Lit(value) => 250 | tokens += tokenFromSpan( 251 | value.span, 252 | SemanticTokenTypes.number 253 | ) 254 | 255 | go(e) 256 | } 257 | 258 | IO.consoleForIO 259 | .errorln( 260 | s"Sending over the following tokens: ${tokens.result().map(_.toString)}" 261 | ) *> 262 | IO.fromEither(encoder.encode(tokens.result())).map(Opt(_)) 263 | 264 | case other => 265 | IO.consoleForIO 266 | .errorln( 267 | s"Got weird state for ${in.textDocument.uri}: $other" 268 | ) 269 | .as(Opt.empty) 270 | 271 | } 272 | } 273 | .handleRequest(textDocument.definition) { (in, back) => 274 | variableUnderCursor(in.textDocument.uri, in.position).map { 275 | foundMaybe => 276 | foundMaybe 277 | .map(_._2) 278 | .map { vdf => 279 | Opt( 280 | Definition( 281 | Location(in.textDocument.uri, vdf.definedAt.toRange) 282 | ) 283 | ) 284 | } 285 | .getOrElse(Opt.empty) 286 | } 287 | } 288 | .handleRequest(textDocument.hover) { (in, back) => 289 | get(in.textDocument.uri).map { 290 | case Some(State.Ok(idx, values, program)) => 291 | idx.variables 292 | .find { case (name, vdf) => 293 | vdf.references.exists(_.contains(in.position.toCaret)) 294 | } 295 | .map { case (varName, vdf) => 296 | val value = values(varName) 297 | val text = program.text.slice( 298 | vdf.fullDefinition.from.offset, 299 | vdf.fullDefinition.to.offset 300 | ) 301 | 302 | Opt { 303 | Hover( 304 | MarkupContent( 305 | kind = MarkupKind.Markdown, 306 | s""" 307 | |`$varName` 308 | |--- 309 | | 310 | |**Value**: $value 311 | | 312 | |**Formula**: $text 313 | """.stripMargin.trim 314 | ) 315 | ) 316 | } 317 | } 318 | .getOrElse(Opt.empty) 319 | 320 | case _ => Opt.empty 321 | } 322 | } 323 | .handleRequest(textDocument.documentSymbol) { (in, back) => 324 | get(in.textDocument.uri).map { 325 | case Some(State.Ok(idx, _, _)) => 326 | Opt { 327 | idx.variables.toVector.sortBy(_._1).map { case (n, df) => 328 | SymbolInformation( 329 | location = 330 | Location(in.textDocument.uri, df.definedAt.toRange), 331 | name = n, 332 | kind = enumerations.SymbolKind.Variable 333 | ) 334 | } 335 | } 336 | 337 | case _ => Opt(Vector.empty) 338 | } 339 | } 340 | .handleRequest(textDocument.rename) { (in, back) => 341 | variableUnderCursor(in.textDocument.uri, in.position).map { 342 | foundMaybe => 343 | foundMaybe 344 | .map { case (oldName, vdf) => 345 | val edits = (vdf.definedAt +: vdf.references).map { span => 346 | TextEdit(range = span.toRange, newText = in.newName) 347 | } 348 | 349 | Opt { 350 | WorkspaceEdit( 351 | changes = Opt( 352 | Map( 353 | in.textDocument.uri -> edits 354 | ) 355 | ) 356 | ) 357 | } 358 | } 359 | .getOrElse(Opt.empty) 360 | } 361 | } 362 | end server 363 | end QuickmaffsLSP 364 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: repl lsp interpreter 2 | 3 | clean: 4 | rm -rf bin/* 5 | scala-cli clean . 6 | 7 | setup: 8 | scala-cli setup-ide *.scala 9 | 10 | 11 | bin/repl: 12 | mkdir -p bin 13 | scala-cli package . --main-class REPL --force --output bin/repl 14 | 15 | bin/lsp: 16 | mkdir -p bin 17 | scala-cli package . --main-class LSP --force --output bin/lsp 18 | 19 | bin/quickmaffs: 20 | mkdir -p bin 21 | scala-cli package . --main-class INTERPRETER --force --output bin/quickmaffs 22 | 23 | repl: bin/repl 24 | lsp: bin/lsp 25 | interpreter: bin/quickmaffs 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quickmaffs 2 | 3 | A small arithmetic language and set of tooling, intended to demonstrate 4 | how easy it is to write Language Servers using [Langoustine](https://github.com/neandertech/langoustine) 5 | and [Jsonrpclib](https://github.com/neandertech/jsonrpclib). 6 | 7 | Through the magic of rabbit holes, the project has a batch interpreter and 8 | a REPL as well for some reason. 9 | 10 | [Scala 3](https://docs.scala-lang.org/scala3/book/introduction.html#), 11 | [Scala CLI](https://scala-cli.virtuslab.org), 12 | [Cats-effect 3](https://typelevel.org/cats-effect/), 13 | [FS2](https://fs2.io/#/) 14 | 15 | ## Pre-requisites 16 | 17 | 1. Ensure you have [Scala CLI](https://scala-cli.virtuslab.org) installed 18 | 19 | ## Build 20 | 21 | * LSP: `make lsp` 22 | * Batch interpreter: `make interpreter` 23 | * REPL: `make repl` 24 | 25 | Everything: `make all` 26 | 27 | ## Usage 28 | 29 | LSP: configure your editor to use `bin/lsp` as a language server for `*.qmf` files 30 | 31 | ![](/docs/lsp.gif) 32 | 33 | REPL: `bin/repl` 34 | 35 | ![](/docs/repl.gif) 36 | 37 | Interpreter 38 | 39 | ![](/docs/interpreter.png) 40 | 41 | -------------------------------------------------------------------------------- /REPL.scala: -------------------------------------------------------------------------------- 1 | import cats.effect.* 2 | import cats.effect.std.Console as CE_Console 3 | import fs2.concurrent.SignallingRef 4 | 5 | import cats.syntax.all.* 6 | import cats.Show 7 | 8 | object Colors: 9 | def red[A](s: A)(using show: Show[A] = Show.fromToString[A]): String = 10 | Console.RED + show.show(s) + Console.RESET 11 | 12 | def green[A](s: A)(using show: Show[A] = Show.fromToString[A]): String = 13 | Console.GREEN + show.show(s) + Console.RESET 14 | 15 | def yellow[A](s: A)(using show: Show[A] = Show.fromToString[A]): String = 16 | Console.YELLOW + show.show(s) + Console.RESET 17 | end Colors 18 | 19 | class REPL(linesIn: fs2.Stream[IO, String], out: String => IO[Unit]): 20 | def outLine[A](s: A)(using show: Show[A] = Show.fromToString) = 21 | out(show.show(s) + "\n") 22 | 23 | def run = 24 | import Colors.* 25 | val state = ( 26 | Program[WithSpan](statements = Vector.empty, text = ""), 27 | Map.empty[String, Int] 28 | ) 29 | fs2.Stream.eval(IO.ref(state)).flatMap { ref => 30 | fs2.Stream.eval(IO.deferred[Either[Throwable, Unit]]).flatMap { halt => 31 | val prompt = out(yellow("> ")) 32 | val intro = 33 | outLine("Welcome to Quickmaffs REPL") *> prompt 34 | 35 | def process(line: String) = 36 | QuickmaffsParser.parseExpr(line) match 37 | case Left(err) => 38 | QuickmaffsParser.parseStatement(line) match 39 | case Left(err) => 40 | outLine(red("Parsing error")) 41 | 42 | case Right(statement) => 43 | ref.get.flatMap { case (prog, state) => 44 | val newProg = Program(prog.statements :+ statement, "") 45 | 46 | QuickmaffsCompiler.compile(newProg) match 47 | case Left(errs) => 48 | errs.traverse(msg => outLine(red("! " + msg.message))) 49 | case Right(idx) => 50 | QuickmaffsEvaluator.evaluate(newProg) match 51 | case Right(interpreted) => 52 | ref.set(newProg -> interpreted) 53 | case Left(err) => 54 | outLine(red("!! " + err.message)) 55 | end match 56 | 57 | } 58 | 59 | case Right(expr) => 60 | ref.get.map(_._2).flatMap { state => 61 | QuickmaffsEvaluator.evaluate(expr, state) match 62 | case Right(value) => 63 | outLine(green(value)) 64 | case Left(err) => 65 | outLine(red("! " + err.message)) 66 | } 67 | 68 | end match 69 | end process 70 | 71 | fs2.Stream.eval(intro) ++ 72 | linesIn 73 | .map(_.trim) 74 | .evalMap { 75 | case e if Set("quit", "exit", "stop").contains(e.toLowerCase()) => 76 | halt.complete(Right(())) 77 | case other => 78 | process(other) *> prompt 79 | } 80 | .interruptWhen(halt) 81 | } 82 | } 83 | 84 | end run 85 | end REPL 86 | 87 | object REPL extends IOApp.Simple: 88 | def run = 89 | REPL( 90 | fs2.io 91 | .stdin[IO](128) 92 | .through(fs2.text.utf8.decode) 93 | .through(fs2.text.lines), 94 | cats.effect.std.Console[IO].print 95 | ).run.compile.drain 96 | end run 97 | end REPL 98 | -------------------------------------------------------------------------------- /compiler.scala: -------------------------------------------------------------------------------- 1 | object QuickmaffsCompiler: 2 | type Id[A] = A 3 | case class CompileError(position: Span, message: String) 4 | 5 | case class VariableDefinition( 6 | definedAt: Span, 7 | fullDefinition: Span, 8 | references: Vector[Span] 9 | ) 10 | 11 | case class Index( 12 | variables: Map[String, VariableDefinition] 13 | ) 14 | object Index: 15 | val empty = Index(Map.empty) 16 | 17 | type Result = Either[Vector[CompileError], Index] 18 | 19 | def compile(p: Program[WithSpan]): Result = 20 | val errors = Vector.newBuilder[CompileError] 21 | 22 | def indexReferences(e: Expr[WithSpan]): Map[String, Vector[Span]] = 23 | inline def pair(e1: Expr[WithSpan], e2: Expr[WithSpan]) = 24 | val m1 = go(e1) 25 | val m2 = go(e2) 26 | val allKeys = m1.keySet ++ m2.keySet 27 | allKeys.map { k => 28 | k -> (m1.getOrElse(k, Vector.empty) ++ m2.getOrElse(k, Vector.empty)) 29 | }.toMap 30 | 31 | def go(expr: Expr[WithSpan]): Map[String, Vector[Span]] = 32 | import Expr.* 33 | expr match 34 | case _: Lit[?] => Map.empty 35 | case Name(WithSpan(pos, name)) => Map(name -> Vector(pos)) 36 | case Add(e1, e2, _) => pair(e1, e2) 37 | case Mul(e1, e2, _) => pair(e1, e2) 38 | 39 | go(e) 40 | end indexReferences 41 | 42 | val idx = p.statements.foldLeft(Index.empty) { 43 | case (idx, WithSpan(span, Statement.Ass(name, e))) => 44 | val varName = name.value.value 45 | val indexed = indexReferences(e) 46 | 47 | val recursiveRefs = indexed.getOrElse(varName, Vector.empty) 48 | 49 | if recursiveRefs.nonEmpty then 50 | errors += CompileError( 51 | recursiveRefs.head, 52 | s"Recrusive reference to variable ${varName} is not allowed" 53 | ) 54 | idx 55 | else if indexed.keySet.exists(ref => !idx.variables.contains(ref)) then 56 | val undefinedVars = 57 | indexed.filter((nm, _) => !idx.variables.contains(nm)) 58 | 59 | undefinedVars.foreach { case (undefined, refs) => 60 | refs.foreach { ref => 61 | errors += CompileError( 62 | ref, 63 | s"Reference to undefined variable ${undefined}" 64 | ) 65 | } 66 | } 67 | 68 | idx 69 | else if !idx.variables.contains(varName) then 70 | idx.copy(variables = 71 | idx.variables 72 | .updated( 73 | varName, 74 | VariableDefinition( 75 | name.value.span, 76 | fullDefinition = span, 77 | Vector.empty 78 | ) 79 | ) 80 | .map { case (name, vdf) => 81 | name -> vdf.copy(references = 82 | vdf.references ++ indexed.getOrElse(name, Vector.empty) 83 | ) 84 | } 85 | ) 86 | else 87 | errors += CompileError( 88 | span, 89 | s"Variable ${varName} has already been defined" 90 | ) 91 | idx.copy(variables = idx.variables.filterNot(_._1 == varName)) 92 | end if 93 | } 94 | 95 | if errors.result().nonEmpty then Left(errors.result()) 96 | else Right(idx) 97 | end compile 98 | end QuickmaffsCompiler 99 | -------------------------------------------------------------------------------- /docs/interpreter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neandertech/quickmaffs/56aedd34ae235de016174e32533ca4f6644f23bf/docs/interpreter.png -------------------------------------------------------------------------------- /docs/lsp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neandertech/quickmaffs/56aedd34ae235de016174e32533ca4f6644f23bf/docs/lsp.gif -------------------------------------------------------------------------------- /docs/repl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neandertech/quickmaffs/56aedd34ae235de016174e32533ca4f6644f23bf/docs/repl.gif -------------------------------------------------------------------------------- /evaluator.scala: -------------------------------------------------------------------------------- 1 | object QuickmaffsEvaluator: 2 | enum EvaluationError(val message: String): 3 | case NameNotFound(name: String) 4 | extends EvaluationError(s"Variable named $name not found") 5 | 6 | import scala.annotation.targetName 7 | @targetName("evalue_WithSpan") 8 | def evaluate( 9 | pg: Program[WithSpan] 10 | ): Either[EvaluationError, Map[String, Int]] = 11 | evaluate(unwrap(pg)) 12 | 13 | type Result = Either[EvaluationError, Map[String, Int]] 14 | 15 | private def evaluate( 16 | pg: Program[Id] 17 | ): Result = 18 | pg.statements.foldLeft[Result](Right(Map.empty[String, Int])) { 19 | case (acc, Statement.Ass(name, expr)) => 20 | acc.flatMap { st => 21 | evaluate(expr, st).map { ev => 22 | st.updated(name.value, ev) 23 | } 24 | } 25 | } 26 | type Id[A] = A 27 | 28 | import cats.syntax.all.* 29 | 30 | def evaluate( 31 | expr: Expr[Id], 32 | state: Map[String, Int] 33 | ): Either[EvaluationError, Int] = 34 | expr match 35 | case Expr.Lit(num) => Right(num) 36 | 37 | case Expr.Name(v) => 38 | state.get(v).toRight(EvaluationError.NameNotFound(v)) 39 | 40 | case Expr.Add(e1, e2, _) => 41 | (evaluate(e1, state), evaluate(e2, state)).mapN(_ + _) 42 | 43 | case Expr.Mul(e1, e2, _) => 44 | (evaluate(e1, state), evaluate(e2, state)).mapN(_ * _) 45 | 46 | @targetName("evaluate_WithSpan") 47 | def evaluate( 48 | expr: Expr[WithSpan], 49 | state: Map[String, Int] 50 | ): Either[EvaluationError, Int] = 51 | evaluate(expr.map([A] => (ws: WithSpan[A]) => ws.value: Id[A]), state) 52 | 53 | private def unwrap(pg: Program[WithSpan]): Program[Id] = 54 | pg.copy(statements = 55 | pg.statements.map( 56 | _.value.map([A] => (ws: WithSpan[A]) => ws.value: Id[A]) 57 | ) 58 | ) 59 | end QuickmaffsEvaluator 60 | -------------------------------------------------------------------------------- /examples/test.qmf: -------------------------------------------------------------------------------- 1 | worsts = (5 + 1) 2 | x = (worsts + 11) 3 | z = 17 4 | test = (x * 1) 5 | h = (x * (11 + worsts)) 6 | hs = (x * (z + worsts)) 7 | ts = 25 8 | a = 11 9 | r = (x + a) 10 | 11 | -------------------------------------------------------------------------------- /examples/test2.qmf: -------------------------------------------------------------------------------- 1 | b = (5 + 1) 2 | x = (b + 11) 3 | s = 17 4 | h = (x * (11 + b)) 5 | hs = (x * (11 + s)) 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /parser.scala: -------------------------------------------------------------------------------- 1 | import cats.parse.Parser as P 2 | import cats.parse.Parser0 as P0 3 | import cats.parse.Caret 4 | import cats.parse.Rfc5234.{sp, alpha, digit, crlf, lf} 5 | 6 | import cats.syntax.all.* 7 | 8 | case class Operator(raw: Char) 9 | 10 | enum Expr[F[_]]: 11 | case Add(l: Expr[F], r: Expr[F], operator: F[Operator]) 12 | case Mul(l: Expr[F], r: Expr[F], operator: F[Operator]) 13 | case Name(value: F[String]) 14 | case Lit(value: F[Int]) 15 | 16 | def map[G[_]](f: [A] => F[A] => G[A]): Expr[G] = 17 | this match 18 | case Lit(num) => Lit(f(num)) 19 | case Name(num) => Name(f(num)) 20 | case Add(e1, e2, o) => Add(e1.map(f), e2.map(f), f(o)) 21 | case Mul(e1, e2, o) => Mul(e1.map(f), e2.map(f), f(o)) 22 | end Expr 23 | 24 | enum Statement[F[_]]: 25 | case Ass(name: Expr.Name[F], e: Expr[F]) 26 | 27 | def map[G[_]](f: [A] => F[A] => G[A]): Statement[G] = 28 | this match 29 | case Ass(nm, e) => 30 | // TODO: how to preserve this? 31 | Ass(nm.map(f).asInstanceOf[Expr.Name[G]], e.map(f)) 32 | 33 | case class Program[F[_]](statements: Vector[F[Statement[F]]], text: String) 34 | 35 | case class Span(from: Caret, to: Caret): 36 | def contains(c: Caret) = 37 | val before = 38 | (from.line < c.line) || (from.line == c.line && from.col <= c.col) 39 | val after = (to.line > c.line) || (to.line == c.line && to.col >= c.col) 40 | 41 | before && after 42 | 43 | case class WithSpan[A](span: Span, value: A) 44 | 45 | object QuickmaffsParser: 46 | case class ParsingError(caret: Caret) 47 | 48 | def parse(s: String): Either[ParsingError, Program[WithSpan]] = 49 | parseWithPosition(statements.map(_.toVector).map(Program.apply(_, s)), s) 50 | 51 | def parseExpr(s: String): Either[ParsingError, Expr[WithSpan]] = 52 | parseWithPosition(expr, s) 53 | 54 | def parseStatement( 55 | s: String 56 | ): Either[ParsingError, WithSpan[Statement[WithSpan]]] = 57 | parseWithPosition(statement, s) 58 | 59 | private def withSpan[A](p: P[A]): P[WithSpan[A]] = 60 | (P.caret.with1 ~ p ~ P.caret).map { case ((start, a), end) => 61 | WithSpan(Span(start, end), a) 62 | } 63 | 64 | private val name: P[Expr.Name[WithSpan]] = 65 | withSpan( 66 | (P.charWhere(_.isLetter) ~ P.charsWhile0(_.isLetter)).map(_.toString + _) 67 | ).map(Expr.Name.apply) 68 | 69 | val litNum = 70 | withSpan(P.charsWhile(_.isDigit).map(_.toInt)).map(Expr.Lit.apply) 71 | 72 | inline def operator(ch: Char): P[WithSpan[Operator]] = 73 | withSpan(P.char(ch).as(Operator(ch))).surroundedBy(sp.rep0) 74 | 75 | val LB = operator('(') 76 | val RB = operator(')') 77 | val PLUS = operator('+') 78 | val MUL = operator('*') 79 | val ASS = operator('=') 80 | val SEMICOLON = operator(';') 81 | 82 | private val expr = P.recursive[Expr[WithSpan]] { recurse => 83 | 84 | inline def pair[F[_]]( 85 | p: P[Expr[F]], 86 | sym: P[F[Operator]] 87 | ): P[(Expr[F], Expr[F], F[Operator])] = 88 | (p, sym, p).tupled.map { case (l, o, r) => (l, r, o) } 89 | 90 | val e = recurse.surroundedBy(sp.rep0) 91 | 92 | val add = pair(e, PLUS).between(LB, RB).map(Expr.Add.apply) 93 | 94 | val mul = pair(e, MUL).between(LB, RB).map(Expr.Mul.apply) 95 | 96 | P.oneOf(add.backtrack :: mul :: litNum :: name :: Nil) 97 | } 98 | 99 | private val assignment = 100 | ((name <* sp.? <* ASS) ~ expr) 101 | .map(Statement.Ass.apply) 102 | .surroundedBy(sp.rep0) 103 | 104 | private val sep = SEMICOLON orElse crlf orElse lf 105 | 106 | private val statement = withSpan(assignment) 107 | 108 | private val statements = statement 109 | .repSep0(sep.rep) 110 | .surroundedBy(sep.rep0) 111 | 112 | private def parseWithPosition[A]( 113 | p: P0[A] | P[A], 114 | s: String 115 | ): Either[ParsingError, A] = 116 | p.parseAll(s).left.map { err => 117 | val offset = err.failedAtOffset 118 | var line = 0 119 | var col = 0 120 | (0 to (offset min s.length)).foreach { i => 121 | if (s(i) == '\n') then 122 | line += 1 123 | col = 0 124 | else col += 1 125 | 126 | } 127 | 128 | ParsingError(Caret(line, col, offset)) 129 | } 130 | 131 | end QuickmaffsParser 132 | -------------------------------------------------------------------------------- /project.scala: -------------------------------------------------------------------------------- 1 | //> using scala "3.2.2" 2 | //> using repository "sonatype-s01:snapshots" 3 | //> using lib "tech.neander::langoustine-app::0.0.19" 4 | //> using lib "org.typelevel::cats-parse::0.3.9" 5 | --------------------------------------------------------------------------------