Skip to content

Commit 82d0d87

Browse files
authored
Jsonnet (#5)
1 parent 31f346a commit 82d0d87

File tree

5 files changed

+201
-0
lines changed

5 files changed

+201
-0
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ libraryDependencies ++= Seq(
1010
"com.lihaoyi" %% "os-lib" % "0.11.4",
1111
"com.lihaoyi" %% "scalatags" % "0.13.1",
1212
"com.lihaoyi" %% "cask" % "0.10.2",
13+
"com.lihaoyi" %% "fastparse" % "3.1.1",
1314
// Java libraries
1415
// scraping
1516
"org.jsoup" % "jsoup" % "1.19.1",

src/main/scala/jsonnet/Expr.scala

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package jsonnet
2+
3+
import jsonnet.Parser.expr
4+
5+
sealed trait Expr
6+
7+
object Expr:
8+
case class Num(value: Int) extends Expr
9+
case class Str(value: String) extends Expr
10+
case class Ident(name: String) extends Expr
11+
case class Plus(left: Expr, right: Expr) extends Expr
12+
case class Dict(pairs: Map[String, Expr]) extends Expr
13+
case class Local(name: String, assigned: Expr, body: Expr) extends Expr
14+
case class Func(argNames: Seq[String], body: Expr) extends Expr
15+
case class Call(expr: Expr, args: Seq[Expr]) extends Expr
16+
17+
def evaluate(expr: Expr, scope: Map[String, Value]): Value =
18+
expr match
19+
case Expr.Ident(name) => scope(name)
20+
case Expr.Num(i) => Value.Num(i)
21+
case Expr.Str(s) => Value.Str(s)
22+
case Expr.Dict(kvs) => Value.Dict(kvs.map { case (k, v) => (k, evaluate(v, scope)) })
23+
24+
case Expr.Plus(left, right) =>
25+
(evaluate(left, scope), evaluate(right, scope)) match
26+
case (Value.Num(leftNum), Value.Num(rightNum)) => Value.Num(leftNum + rightNum)
27+
case (Value.Str(leftStr), Value.Str(rightStr)) => Value.Str(leftStr + rightStr)
28+
29+
case Expr.Local(name, assigned, body) =>
30+
val assignedValue = evaluate(assigned, scope)
31+
evaluate(body, scope + (name -> assignedValue))
32+
33+
case Expr.Call(expr, args) =>
34+
val Value.Func(call) = evaluate(expr, scope)
35+
val evaluatedArgs = args.map(evaluate(_, scope))
36+
call(evaluatedArgs)
37+
38+
case Expr.Func(argNames, body) =>
39+
Value.Func(args => evaluate(body, scope ++ argNames.zip(args).toMap))
40+
41+
// Use this for printing as a compact json string
42+
private def serialize(v: Value): String =
43+
v match
44+
case Value.Num(i) => i.toString
45+
case Value.Str(s) => s"\"$s\""
46+
case Value.Dict(kvs) => kvs.map((k, v) => s"\"$k\": ${serialize(v)}").mkString("{", ", ", "}")
47+
48+
// Use this for pretty printing
49+
private def serialize2(v: Value): ujson.Value =
50+
v match
51+
case Value.Num(i) => ujson.Num(i)
52+
case Value.Str(s) => ujson.Str(s)
53+
case Value.Dict(kvs) => ujson.Obj.from(kvs.map { case (k, v) => (k, serialize2(v)) })
54+
55+
def jsonnet(input: String): String =
56+
// serialize(evaluate(fastparse.parse(input, expr(_)).get.value, Map.empty))
57+
ujson.write(
58+
serialize2(evaluate(fastparse.parse(input, Parser.expr(_)).get.value, Map.empty)),
59+
indent = 2
60+
)
61+
62+
def main(args: Array[String]): Unit = {
63+
Seq(
64+
evaluate(fastparse.parse("\"hello\"", expr(_)).get.value, Map.empty),
65+
evaluate(fastparse.parse("""{ "hello": "world", "key": "value" }""", expr(_)).get.value, Map.empty),
66+
evaluate(fastparse.parse("\"hello\" + \"world\"", expr(_)).get.value, Map.empty),
67+
68+
// Call
69+
evaluate(fastparse.parse("""local greeting = "hello "; greeting + greeting""", expr(_)).get.value, Map.empty),
70+
evaluate(fastparse.parse("""local x = "Hello "; local y = "World"; x + y""", expr(_)).get.value, Map.empty),
71+
// evaluate(fastparse.parse("""local greeting = "Hello"; nope + nope""", expr(_)).get.value, Map.empty),
72+
73+
// Func
74+
evaluate(fastparse.parse("""local f = function(a) a + "1"; f("123")""", expr(_)).get.value, Map.empty),
75+
evaluate(
76+
fastparse
77+
.parse("""local f = function(a, b) a + " " + b; f("hello", "world")""", expr(_))
78+
.get
79+
.value,
80+
Map.empty
81+
)
82+
).foreach(println)
83+
84+
// jsonnet
85+
println(jsonnet("""|local greeting = "Hello ";
86+
|local person = function (name) {
87+
| "name": name,
88+
| "welcome": greeting + name + "!"
89+
|};
90+
|{
91+
| "person1": person("Alice"),
92+
| "person2": person("Bob"),
93+
| "person3": person("Charlie")
94+
|}""".stripMargin))
95+
96+
println(
97+
jsonnet(
98+
"""|local bonus = 15000;
99+
|local person = function (name, baseSalary) {
100+
| "name": name,
101+
| "totalSalary": baseSalary + bonus
102+
|};
103+
|{"person1": person("Alice", 10000), "person2": person("Bob", 20000)}
104+
|""".stripMargin
105+
)
106+
)
107+
}
108+
end Expr

src/main/scala/jsonnet/Parser.scala

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package jsonnet
2+
3+
import fastparse.*
4+
import MultiLineWhitespace.*
5+
6+
object Parser:
7+
8+
def num[p: P]: P[Expr.Num] = P(CharIn("0-9").rep(1).!.map(_.toInt)).map(Expr.Num.apply)
9+
def str0[p: P]: P[String] = P("\"" ~~/ CharsWhile(_ != '"', 0).! ~~ "\"")
10+
def str[p: P]: P[Expr.Str] = P(str0).map(Expr.Str.apply)
11+
def ident0[p: P]: P[String] = P(CharIn("a-zA-Z_") ~~ CharsWhileIn("a-zA-z0-9_", 0)).!
12+
def ident[p: P]: P[Expr.Ident] = P(ident0).map(Expr.Ident.apply)
13+
def local[p: P]: P[Expr.Local] = P("local" ~/ ident0 ~ "=" ~ expr ~ ";" ~ expr).map(Expr.Local.apply)
14+
def func[p: P]: P[Expr.Func] = P("function" ~/ "(" ~ ident0.rep(0, ",") ~ ")" ~ expr).map(Expr.Func.apply)
15+
16+
def dict[p: P]: P[Expr.Dict] = P("{" ~/ (str0 ~ ":" ~/ expr).rep(0, ",") ~/ "}").map(kvs => Expr.Dict(kvs.toMap))
17+
18+
def callExpr[p: P]: P[Expr] = P(num | str | dict | local | func | ident)
19+
def call[p: P]: P[Seq[Expr]] = P("(" ~/ expr.rep(0, ",") ~ ")")
20+
21+
def prefixExpr[p: P]: P[Expr] = P(callExpr ~ call.rep).map { case (left, items) =>
22+
items.foldLeft(left)(Expr.Call.apply)
23+
}
24+
25+
def plus[p: P]: P[Expr] = P("+" ~ prefixExpr)
26+
def expr[p: P]: P[Expr] = P(prefixExpr ~ plus.rep).map { case (left, rights) =>
27+
rights.foldLeft(left)(Expr.Plus.apply)
28+
}
29+
30+
def main(args: Array[String]): Unit = {
31+
Seq( // str
32+
fastparse.parse("\"hello\"", str(_)),
33+
fastparse.parse("\"hello world\"", str(_)),
34+
fastparse.parse("\"\"", str(_)),
35+
fastparse.parse("123", str(_))
36+
).foreach(println)
37+
38+
Seq( // ident
39+
fastparse.parse("hello", ident(_)),
40+
fastparse.parse("_world", ident(_)),
41+
fastparse.parse("hello world", ident(_)),
42+
fastparse.parse("123", ident(_))
43+
).foreach(println)
44+
45+
Seq( // plus
46+
fastparse.parse("\"hello\" + \"world\"", expr(_)),
47+
fastparse.parse("\"hello\" + \" \" + \"world\"", expr(_)),
48+
fastparse.parse("a + b", expr(_)),
49+
fastparse.parse("""a + " " + c""", expr(_))
50+
).foreach(println)
51+
52+
Seq( // dict
53+
fastparse.parse("""{"a": "b", "cde": id, "nested": {}}""", dict(_)),
54+
fastparse.parse("""{"a": "A", "b": "bee"}""", expr(_)),
55+
fastparse.parse("""f()(a) + g(b, c)""", expr(_)),
56+
fastparse.parse("""local thing = "kay"; { "f": function(a) a + a, "nested": {"k": "v"}}""", expr(_)),
57+
58+
// Case for exercise 1
59+
fastparse.parse("""local thing = 100; """, expr(_)),
60+
fastparse.parse(
61+
"""|local bonus = 15000;
62+
|local person = function (name, baseSalary) {
63+
| "name": name,
64+
| "totalSalary": baseSalary + bonus
65+
|};
66+
|{"person1": person("Alice", 10000), "person2": person("Bob", 20000)}
67+
|""".stripMargin,
68+
expr(_)
69+
)
70+
).foreach(println)
71+
}

src/main/scala/jsonnet/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Jsonnet
2+
3+
Exercises:
4+
5+
- Modify our Jsonnet interpreter to be able to handle basic numbers as well ...
6+
7+
Track `Value.Num` and `Expr.Num`
8+
9+
- Modify def serialize to ... pretty print the output JSON ...
10+
11+
See `Expr.serialize2` and its use in `Expr.jsonnet`

src/main/scala/jsonnet/Value.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package jsonnet
2+
3+
sealed trait Value
4+
5+
object Value:
6+
case class Str(value: String) extends Value
7+
case class Num(value: Int) extends Value
8+
case class Dict(pairs: Map[String, Value]) extends Value
9+
case class Func(call: Seq[Value] => Value) extends Value
10+
end Value

0 commit comments

Comments
 (0)