L1 — Reading an expression
Goal: take a string a user typed, turn it into an expression, and handle every way that can fail — including the one that does not look like failure at all.
Add the library
libraryDependencies += "it.grypho" %% "leonardo" % "0.0.0+1-92b231cd-SNAPSHOT"
That is the library. There is a second artifact, leonardo-repl, which you only need if you want to embed the REPL itself — Getting Started explains the split, and L8 comes back to it.
Want to feel the expression language before writing code against it? The browser REPL needs no installation, and Lessons: at the prompt covers the same grammar interactively.
Parse something
import it.grypho.scala.leonardo.parser.Parser
Parser.parse("sin(x)^2 + cos(x)^2")
// res0: ParseResult[_Expression] = Success(
// result = Sum(
// a = Power(base = Sin(_Variable("x")), exp = _Number(2.0)),
// b = Power(base = Cos(_Variable("x")), exp = _Number(2.0))
// ),
// next = CharSequenceReader()
// )
The tree is immutable, so one parse can be evaluated many times with different bindings — which is what L2 and L3 do with it.
Why ParseResult and not Option or Either?
Because it is not Leonardo’s type. Parser is built on the standard parser-combinator library, and ParseResult[_Expression] is that library’s own result — three cases, Success, Failure and Error, carrying position information a bare Option would throw away.
You will want to convert it at the boundary, and this lesson does exactly that. For reading one directly, successful is the predicate:
Parser.parse("sin(").successful
// res1: Boolean = false
This is the only place in Leonardo where a foreign result type surfaces. Everything past the parser speaks Either, Option or the AST itself, which is what L2 picks up.
Handle the failure, not just the success
You will see .get in short examples, including on the Getting Started page. It is fine for a one-line demonstration and wrong in a program: on a Failure it throws, and the input to a parser is by definition something you do not control.
So match on the outcome. The Try around the call is belt and braces — it is what the REPL does with every line it reads, because input you did not write can be pathological in ways a parser does not promise to survive:
import scala.util.Try
import it.grypho.scala.leonardo.core._Expression
/** Reads user input into an expression, or explains why it could not. */
def read(text: String): Either[String, _Expression] =
Try(Parser.parse(text)).fold(
error => Left(s"parse error: ${error.getMessage}"),
result =>
if result.successful then Right(result.get)
else Left(s"parse error: ${result.toString.linesIterator.next()}")
)
Both paths now produce a message instead of an exception:
read("2 * (x + 1)")
// res2: Either[String, _Expression] = Right(
// Product(a = _Number(2.0), b = Sum(a = _Variable("x"), b = _Number(1.0)))
// )
read("sin(")
// res3: Either[String, _Expression] = Left(
// "parse error: [1.5] failure: '(' expected but end of source found"
// )
Deeply nested input is worth trying, because it shows the library’s habit rather than a crash:
// 300 nested brackets, against a nesting limit of 200.
read("(" * 300 + "1" + ")" * 300)
// res4: Either[String, _Expression] = Left(
// "parse error: [1.202] failure: expression exceeds maximum nesting depth of 200"
// )
That is a refusal, not an accident — the parser guards its own nesting depth and reports hitting it in the same shape as any other parse error, so your one handler covers it. You will meet that pattern throughout Leonardo: when it cannot do something, it says so rather than guessing or exploding.
This is the shape the REPL itself uses — one funnel that every piece of input passes through — and it is worth writing once in your own code for the same reason.
The failure that is not a failure
Here is the one thing about this parser you should learn before you write anything serious:
read("sqrt(x)").map(_.toString)
// res5: Either[String, String] = Right("(sqrt * x)")
That parsed. It did not fail, and it did not warn.
There is no sqrt in the grammar, and an unrecognised name is a perfectly good variable. Juxtaposition means multiplication, so sqrt(x) is the variable sqrt times the variable x — a legitimate expression that will evaluate, simplify and round-trip without ever mentioning that you meant a square root. (The square root is written x^0.5 or x^(1/2).)
The consequence is a rule:
The grammar has no “unknown identifier” error. If you accept text from a user, validate the names yourself — do not hand the string to the parser and wait for it to object.
Parser.ReservedWords is the set the grammar actually claims, and it is what to check against:
Parser.ReservedWords.contains("sin")
// res6: Boolean = true
Parser.ReservedWords.contains("sqrt")
// res7: Boolean = false
Anything written immediately before a ( that is not in that set is a name the grammar does not know. Checking it is cheap, and it turns a confidently wrong answer into an error message — which is the trade this library makes everywhere.
Try it yourself
Write named(text: String): Either[String, List[String]] that parses text and returns the free variable names it contains, sorted — or the parse error. Every expression exposes freeVars.
Answers
```scala def named(text: String): Either[String, List[String]] = read(text).map(_.freeVars.toList.sorted) ``` ```scala named("a * sin(b) + c") // res8: Either[String, List[String]] = Right(List("a", "b", "c")) ``` ```scala named("sqrt(x)") // two names, which is the lesson's point // res9: Either[String, List[String]] = Right(List("sqrt", "x")) ``` ```scala named("sin(") // res10: Either[String, List[String]] = Left( // "parse error: [1.5] failure: '(' expected but end of source found" // ) ```What you learned
Parser.parsereturns aParseResult— the combinator library’s type, not Leonardo’s. Checksuccessfulbeforeget, and convert at the boundary.- Input that is too deeply nested is refused with an ordinary parse error, not a crash — one handler covers it, and refusing rather than guessing is a habit you will meet throughout.
- An unknown name parses as a variable. Validate names against
Parser.ReservedWordsyourself, because the grammar will never complain.
Next: L2 — The dual evaluation model, where an expression becomes either a concrete value or a smaller expression. · All lessons