Hi(experimental language) lang interpreter + repl
In this homework we will gradually develop a small programming language called Hi.
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunDiv | HiFunMul | HiFunAdd | HiFunSub data HiValue = ... | HiValueNumber Rational | HiValueFunction HiFun data HiExpr = ... | HiExprValue HiValue | HiExprApply HiExpr [HiExpr] data HiError = ... | HiErrorInvalidArgument | HiErrorInvalidFunction | HiErrorArityMismatch | HiErrorDivideByZero
Numbers are represented using the
Rational
type fromData.Ratio
. -
In the parser, add support for the following constructs:
-
Built-in names
div
,mul
,add
, andsub
, that are parsed into the correspondingHiFun
constructors (and then wrapped inHiValueFunction
). -
Numeric literals, such as
2
,3.14
,-1.618
, or1.2e5
, that are parsed intoHiValueNumber
(tip: useText.Megaparsec.Char.Lexer.scientific
). -
Function application
f(a, b, c, ...)
that is parsed intoHiExprApply
.
For example, the expression
div(add(10, 15.1), 3)
is represented by the following syntax tree:HiExprApply (HiExprValue (HiValueFunction HiFunDiv)) [ HiExprApply (HiExprValue (HiValueFunction HiFunAdd)) [ HiExprValue (HiValueNumber (10 % 1)), HiExprValue (HiValueNumber (151 % 10)) ], HiExprValue (HiValueNumber (3 % 1)) ]
-
-
In the evaluator, implement the arithmetic operations:
add(500, 12)
evaluates to512
(addition)sub(10, 100)
evaluates to-90
(subtraction)mul(23, 768)
evaluates to17664
(multiplication)div(57, 190)
evaluates to0.3
(division)
Nested function applications are allowed:
div(add(mul(2, 5), 1), sub(11,6))
evaluates to2.2
The following errors must be returned as
HiError
:HiErrorArityMismatch
: functions called with an incorrect amount of arguments, e.g.sub(1)
orsub(1, 2, 3)
.HiErrorDivideByZero
: thediv
function is called with0
as its second argument, e.g.div(1, 0)
ordiv(1, sub(5, 5))
.HiErrorInvalidFunction
: numbers are used in function positions, e.g.15(2)
.HiErrorInvalidArgument
: functions are used in numeric positions, e.g.sub(10, add)
.
You are advised to use the
ExceptT
monad transformer to propagateHiError
through the evaluator. -
In the pretty-printer, define the following special cases for rendering numbers:
- integers:
42
,-8
,15
- finite decimal fractions:
3.14
,-8.15
,77.01
- fractions:
1/3
,-1/7
,3/11
- mixed fractions:
5 + 1/3
,-10 - 1/7
,24 + 3/11
You will find these functions useful:
quotRem
fromPrelude
fromRationalRepetendUnlimited
from thescientific
package
- integers:
The following session in the REPL should be possible if you have implemented all of the above correctly:
hi> 100
100
hi> -15
-15
hi> add(100, -15)
85
hi> add(3, div(14, 100))
3.14
hi> div(10, 3)
3 + 1/3
hi> sub(mul(201, 11), 0.33)
2210.67
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunNot | HiFunAnd | HiFunOr | HiFunLessThan | HiFunGreaterThan | HiFunEquals | HiFunNotLessThan | HiFunNotGreaterThan | HiFunNotEquals | HiFunIf data HiValue = ... | HiValueBool Bool
-
In the parser, add support for the following constructs:
-
Built-in names
not
,and
,or
,less-than
,greater-than
,equals
,not-less-than
,not-greater-than
,not-equals
,if
, that are parsed into the correspondingHiFun
constructors. -
Built-in names
true
andfalse
that are parsed intoHiValueBool
.
-
-
In the evaluator, implement the new operations.
Boolean algebra:
not(true)
evaluates tofalse
(negation)and(true, false)
evaluates tofalse
(conjunction)or(true, false)
evaluates totrue
(disjunction)
Equality checking:
equals(10, 10)
evaluates totrue
equals(false, false)
evaluates totrue
equals(3, 10)
evaluates tofalse
equals(1, true)
evaluates tofalse
(no implicit cast)
Comparisons:
less-than(3, 10)
evaluates totrue
less-than(false, true)
evaluates totrue
less-than(false, 0)
evaluates totrue
(Bool
is less thanNumber
)
Complements:
- for all
A
B
,greater-than(A, B) ≡ less-than(B, A)
holds - for all
A
B
,not-equals(A, B) ≡ not(equals(A, B))
holds - for all
A
,B
,not-less-than(A, B) ≡ not(less-than(A, B))
holds - for all
A
,B
,not-greater-than(A, B) ≡ not(greater-than(A, B))
holds
Branching:
- for all
A
B
,if(true, A, B) ≡ A
holds - for all
A
B
,if(false, A, B) ≡ B
holds
The following session in the REPL should be possible:
hi> false
false
hi> equals(add(2, 2), 4)
true
hi> less-than(mul(999, 99), 10000)
false
hi> if(greater-than(div(2, 5), div(3, 7)), 1, -1)
-1
hi> and(less-than(0, 1), less-than(1, 0))
false
Note also that functions are values:
hi> if(true, add, mul)
add
hi> if(true, add, mul)(10, 10)
20
hi> if(false, add, mul)(10, 10)
100
Functions can also be tested for equality:
hi> equals(add, add)
true
hi> equals(add, mul)
false
The check is trivial: a function is equal only to itself.
Ordering of function symbols is implementation-defined, that is, it’s up to you whether less-than(add, mul)
or greater-than(add, mul)
.
In the parser, add support for infix operators. The precedence and associativity are the same as in Haskell.
For all A B
:
A / B
parses todiv(A, B)
A * B
parses tomul(A, B)
A + B
parses toadd(A, B)
A - B
parses tosub(A, B)
A < B
parses toless-than(A, B)
A > B
parses togreater-than(A, B)
A >= B
parses tonot-less-than(A, B)
A <= B
parses tonot-greater-than(A, B)
A == B
parses toequals(A, B)
A /= B
parses tonot-equals(A, B)
A && B
parses toand(A, B)
A || B
parses toor(A, B)
Tip: use makeExprParser
from the parser-combinators
package.
The following session in the REPL should be possible:
hi> 2 + 2
4
hi> 2 + 2 * 3
8
hi> (2 + 2) * 3
12
hi> 2 + 2 * 3 == (2 + 2) * 3
false
hi> 10 == 2*5 && 143 == 11*13
true
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunLength | HiFunToUpper | HiFunToLower | HiFunReverse | HiFunTrim data HiValue = ... | HiValueNull | HiValueString Text
Strings are represented using the
Text
type from thetext
package. -
In the parser, add support for the following constructs:
-
Bulit-in names
length
,to-upper
,to-lower
,reverse
,trim
, that are parsed into the correspondingHiFun
constructors. -
Built-in name
null
that is parsed intoHiValueNull
. -
String literals, such as
"hello"
,"42"
, or"header\nfooter"
, that are parsed intoHiValueString
(tip: useText.Megaparsec.Char.Lexer.charLiteral
).
-
-
In the evaluator, implement the new operations:
length("Hello World")
evaluates to11
to-upper("Hello World")
evaluates to"HELLO WORLD"
to-lower("Hello World")
evaluates to"hello world"
reverse("stressed")
evaluates to"desserts"
trim(" Hello World ")
evaluates to"Hello World"
Then overload existing operations to work on strings:
"Hello" + "World"
evaluates to"HelloWorld"
"Cat" * 5
evaluates to"CatCatCatCatCat"
(tip: usestimes
)"/home/user" / "hi"
evaluates to"/home/user/hi"
When a string is used as a function of one argument, perform a lookup:
"Hello World"(0)
evaluates to"H"
"Hello World"(7)
evaluates to"o"
Out-of-bounds indexing returns
null
:"Hello World"(-1)
evaluates tonull
"Hello World"(99)
evaluates tonull
When a string is used as a function of two arguments, take a slice:
"Hello World"(0, 5)
evaluates to"Hello"
"Hello World"(2, 4)
evaluates to"ll"
-
(Advanced) When a slice index is negative, implement the Python semantics of indexing from the end of the string:
"Hello World"(0, -4)
evaluates to"Hello W"
"Hello World"(-4, -1)
evaluates to"orl"
When a slice index is
null
, treat it as the start/end of the string:"Hello, World"(2, null)
evaluates to"llo, World"
"Hello, World"(null, 5)
evaluates to"Hello"
The following session in the REPL should be possible:
hi> to-upper("what a nice language")(7, 11)
"NICE"
hi> "Hello" == "World"
false
hi> length("Hello" + "World")
10
hi> length("hehe" * 5) / 3
6 + 2/3
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunList | HiFunRange | HiFunFold data HiValue = ... | HiValueList (Seq HiValue)
Lists are represented using the
Seq
type from thecontainers
package. -
In the parser, add support for the following constructs:
-
Built-in names
list
,range
,fold
, that are parsed into the correspondingHiFun
constructors. -
List literals, written as
[A, B, C, ...]
, that are parsed into function applicationlist(A, B, C, ...)
.
-
-
In the evaluator, implement the new operations:
list(1, 2, 3)
constructsHiValueList
containing1
,2
,3
range(5, 10.3)
evaluates to[5, 6, 7, 8, 9, 10]
fold(add, [11, 22, 33])
evaluates to66
fold(mul, [11, 22, 33])
evaluates to7986
fold(div, [11, 22, 33])
evaluates to1/66
(left fold)
Then overload existing operations to work on lists:
length([1, true, "Hello"])
evaluates to3
reverse([1, true, "Hello"])
evaluates to["Hello", true, 1]
[1, 2] + [3, 4, 5]
evaluates to[1, 2, 3, 4, 5]
[0, "x"] * 3
evaluates to[0, "x", 0, "x", 0, "x"]
(tip: usestimes
)
When a list is used as a function, perform indexing/slicing:
["hello", true, "world"](1)
evaluates totrue
["hello", true, "world"](1,3)
evaluates to[true, "world"]
The following session in the REPL should be possible:
hi> list(1, 2, 3, 4, 5)
[ 1, 2, 3, 4, 5 ]
hi> fold(add, [2, 5] * 3)
21
hi> fold(mul, range(1, 10))
3628800
hi> [0, true, false, "hello", "world"](2, 4)
[ false, "hello" ]
hi> reverse(range(0.5, 70/8))
[ 8.5, 7.5, 6.5, 5.5, 4.5, 3.5, 2.5, 1.5, 0.5 ]
Lists of bytes (numbers from 0
to 255
) can be represented and processed more efficiently. Let us introduce a new value type for them.
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunPackBytes | HiFunUnpackBytes | HiFunEncodeUtf8 | HiFunDecodeUtf8 | HiFunZip | HiFunUnzip | HiFunSerialise | HiFunDeserialise data HiValue = ... | HiValueBytes ByteString
Bytes are represented using the strict
ByteString
type from thebytestring
package. -
In the parser, add support for the following constructs:
-
Built-in names
pack-bytes
,unpack-bytes
,zip
,unzip
,encode-utf8
,decode-utf8
,serialise
, anddeserialise
, that are parsed into the correspondingHiFun
constructors. -
Byte array literals, such as
[# 01 3f ec #]
that are parsed intoHiValueBytes
. Each element is a two-digit hexadecimal number.
-
-
In the evaluator, implement the new operations:
pack-bytes([ 3, 255, 158, 32 ])
evaluates to[# 03 ff 9e 20 #]
unpack-bytes([# 10 20 30 #])
evaluates to[16, 32, 48]
encode-utf8("Hello!")
evaluates to[# 48 65 6c 6c 6f 21 #]
decode-utf8([# 48 65 6c 6c 6f #])
evaluates to"Hello"
decode-utf8([# c3 28 #])
evaluates tonull
(invalid UTF-8 byte sequence)zip
compresses the bytes using thezlib
package (specifybestCompression
)serialise
turns any value into bytes using theserialise
package- for all
A
,unzip(zip(A)) ≡ A
holds - for all
A
,deserialise(serialise(A)) ≡ A
holds
Then overload existing operations to work on bytes:
[# 00 ff #] + [# 01 e3 #]
evaluates to[# 00 ff 01 e3 #]
[# 00 ff #] * 3
evaluates to[# 00 ff 00 ff 00 ff #]
(tip: usestimes
)
When bytes are used as a function, perform indexing/slicing as with strings and lists:
[# 00 ff 01 e3 #](1)
evaluates to255
[# 00 ff 01 e3 #](1,3)
evaluates to[# ff 01 #]
The following session in the REPL should be possible:
hi> pack-bytes(range(30, 40))
[# 1e 1f 20 21 22 23 24 25 26 27 28 #]
hi> zip(encode-utf8("Hello, World!" * 1000))
[# 78 da ed c7 31 0d 00 20 0c 00 30 2b f0 23 64 0e 30 00 df 92 25 f3 7f a0 82 af
fd 1a 37 b3 d6 d8 d5 79 66 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88
88 88 88 88 88 88 88 88 fc c9 03 ca 0f 3b 28 #]
hi> decode-utf8([# 68 69 #] * 5)
"hihihihihi"
hi> unzip([# 78 da 63 64 62 06 00 00 0d 00 07 #])
[# 01 02 03 #]
In this task we extend the language with I/O capabilities. We consider it to be the most important part of the homework and it is graded with extra points.
Let us start by creating a new type in HW3.Base
that encodes the available actions:
data HiAction =
HiActionRead FilePath
| HiActionWrite FilePath ByteString
| HiActionMkDir FilePath
| HiActionChDir FilePath
| HiActionCwd
Now recall that the type of our eval
function is as follows:
eval :: Monad m => HiExpr -> m (Either HiError HiValue)
We could just require m
to be IO
in order to execute the actions, but that would be bad design, as it would make it impossible to do evaluation in a pure, deterministic context (e.g. for tests). Instead, let us create a new class in HW3.Base
:
class Monad m => HiMonad m where
runAction :: HiAction -> m HiValue
One could imagine at least a few possible instances of this class:
-
IO
, where those actions could interact with the actual file system -
Identity
, where the actions do nothing and returnnull
-
State FS
, whereFS
is a pure and deterministic in-memory simulation of the file system -
ReaderT Permissions IO
, which can be more secure thanIO
by controlling whether the program has read-only or read-write access
While it would be useful to implement all of those, we shall limit ourselves to the last one to avoid making the task unnecessarily tedious.
In a new module HW3.Action
, declare the following:
data HiPermission =
AllowRead
| AllowWrite
data PermissionException =
PermissionRequired HiPermission
instance Exception PermissionException
newtype HIO a = HIO { runHIO :: Set HiPermission -> IO a }
Finally, let us change the type of eval
as follows:
- eval :: Monad m => HiExpr -> m (Either HiError HiValue)
+ eval :: HiMonad m => HiExpr -> m (Either HiError HiValue)
With those preliminaries out of the way, we can start integrating the actions into the rest of the language.
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunRead | HiFunWrite | HiFunMkDir | HiFunChDir data HiValue = ... | HiValueAction HiAction data HiExpr = ... | HiExprRun HiExpr
-
In the parser, add support for the following constructs:
-
Built-in names
read
,write
,mkdir
,cd
, that are parsed into the correspondingHiFun
constructors. -
Built-in name
cwd
that is parsed intoHiValueAction HiActionCwd
. -
E!
notation that is parsed intoHiExprRun
, e.g.read("hello.txt")!
,mkdir("projects")!
, orcwd!
.
-
-
In the evaluator, implement the new functions to return the corresponding actions:
read("hi.txt")
evaluates toread("hi.txt")
. While visually the same, internally the first one isHiExprApply
and the second one isHiValueAction
.write("hi.txt", "Hi!")
evaluates towrite("hi.txt", [# 48 69 21 #])
mkdir("dir")
evaluates tomkdir("dir")
cd("dir")
evaluates tocd("dir")
Then implement the
HiExprRun
construct, which should execute the action usingrunAction
that we defined earlier. -
Define the
HiMonad HIO
instance, such that:cwd!
returns the current working directorycd("mydir")!
changes the current working directory tomydir
read("myfile")!
returns the contents ofmyfile
(useHiValueString
if the contents are valid UTF-8 andHiValueBytes
otherwise)read("mydir")!
returns the directory listing ofmydir
write("myfile", "Hello")!
writes"Hello"
tomyfile
mkdir("mydir")!
creates a new directorymydir
Use the
directory
package to implement all of the above. -
Implement permission control in
HiMonad HIO
, so that actions throwPermissionException
(usingthrowIO
) unless they are allowed.AllowRead
enablescwd
,cd
,read
AllowWrite
enableswrite
,mkdir
The following session in the REPL should be possible:
hi> mkdir("tmp")!
null
hi> read("tmp")!
[]
hi> mkdir("tmp/a")!
null
hi> mkdir("tmp/b")!
null
hi> read("tmp")!
[ "a", "b" ]
hi> write("tmp/hi.txt", "Hello")!
null
hi> cd("tmp")!
null
hi> read("hi.txt")!
"Hello"
Note that actions are just values and only !
forces their execution:
hi> read
read
hi> read("hi.txt")
read("hi.txt")
hi> read("hi.txt")!
"Hello"
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunParseTime data HiValue = ... | HiValueTime UTCTime data HiAction = ... | HiActionNow
Time is represented using the
UTCTime
type from thetime
package.Extend the data types in
HW3.Action
to include the following constructors:data HiPermission = ... | AllowTime
-
In the parser, add support for the following constructs:
-
Built-in name
parse-time
that is parsed into the correspondingHiFun
constructor. -
Built-in name
now
that is parsed into the correspondingHiAction
constructor.
-
-
In the evaluator, implement
parse-time
usingreadMaybe
to parse aHiValueString
into aHiValueTime
, orHiValueNull
in case of failure. -
In the
HiMonad HIO
instance, implement theHiActionNow
to return the current system time. It requires theAllowTime
permission. -
In the evaluator, overload existing operations to work on time:
parse-time("2021-12-15 00:00:00 UTC") + 1000
evaluates toparse-time("2021-12-15 00:16:40 UTC")
(useaddUTCTime
)parse-time("2021-12-15 00:37:51.000890793 UTC") - parse-time("2021-12-15 00:37:47.649047038 UTC")
evaluates to3.351843755
(usediffUTCTime
)
The following session in the REPL should be possible:
hi> now!
parse-time("2021-12-15 00:42:33.02949461 UTC")
hi> parse-time("2021-01-01 00:00:00 UTC") + 365 * 24 * 60 * 60
parse-time("2022-01-01 00:00:00 UTC")
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunRand data HiAction = ... | HiActionRand Int Int
-
In the parser, add support for the built-in name
rand
that is parsed into the correspondingHiFun
constructor. -
In the evaluator, implement the new function:
rand(0, 10)
evaluates torand(0, 10)
. While visually the same, internally the first one isHiExprApply
and the second one isHiValueAction
.
-
Extend the
HiMonad HIO
instance, so that:rand(0, 5)!
evaluates to0
,1
,2
,3
,4
, or5
- the distribution of random numbers is uniform
Tip: use the
random
package.
The following session in the REPL should be possible:
hi> rand
rand
hi> rand(0, 10)
rand( 0, 10 )
hi> rand(0, 10)!
8
hi> rand(0, 10)!
3
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunEcho data HiAction = ... | HiActionEcho Text
-
In the parser, add support for the built-in name
echo
that is parsed into the correspondingHiFun
constructor. -
In the evaluator, implement the new function:
echo("Hello")
evaluates toecho("Hello")
. While visually the same, internally the first one isHiExprApply
and the second one isHiValueAction
.
-
Extend the
HiMonad HIO
instance, so thatecho("Hello")!
printsHello
followed by a newline tostdout
. It requires theAllowWrite
permission. -
In the evaluator, ensure that
if(true, A, B)
does not evaluateB
, andif(false, A, B)
does not evaluateA
.Then generalise
A && B
as follows:- if
A
isfalse
ornull
, returnA
without evaluatingB
- otherwise, evaluate and return
B
Generalise
A || B
as follows:- if
A
isfalse
ornull
, evaluate and returnB
- otherwise, return
A
without evaluatingB
- if
The following session in the REPL should be possible:
hi> echo
echo
hi> echo("Hello")
echo("Hello")
hi> echo("Hello")!
Hello
null
hi> "Hello"(0) || "Z"
"H"
hi> "Hello"(99) || "Z"
"Z"
hi> if(2 == 2, echo("OK")!, echo("WTF")!)
OK
null
hi> true || echo("Don't do this")!
true
hi> false && echo("Don't do this")!
false
hi> [# 00 ff #] && echo("Just do it")!
Just do it
null
-
Extend the data types in
HW3.Base
to include the following constructors:data HiFun = ... | HiFunCount | HiFunKeys | HiFunValues | HiFunInvert data HiValue = ... | HiValueDict (Map HiValue HiValue) data HiExpr = ... | HiExprDict [(HiExpr, HiExpr)]
Dictionaries are represented using the
Map
type from thecontainers
package. -
In the parser, add support for the following constructs:
-
Built-in names
count
,keys
,values
,invert
, that are parsed into the correspondingHiFun
constructors. -
Dictionary literals, written as
{ I: A, J: B, K: C }
, for example:{ "width": 120, "height": 80 }
{ 1: true, 3: true, 4: false }
-
Dot access, written as
E.fld
, that is parsed into function applicationE("fld")
. For example, the following holds:{ "width": 120, "height": 80 }.width ≡ { "width": 120, "height": 80 }("width")
-
-
In the evaluator, implement the new operations:
{ "width": 120, "height": 80 }("width")
evaluates to120
keys({ "width": 120, "height": 80 })
evaluates to["height", "width"]
(sorted)values({ "width": 120, "height": 80 })
evaluates to[80, 120]
(sorted by key)count("XXXOX")
evaluates to{ "O": 1, "X": 4 }
count([# 58 58 58 4f 58 #])
evaluates to{ 79: 1, 88: 4 }
count([true, true, false, true])
evaluates to{ false: 1, true: 3 }
invert({ "x": 1, "y" : 2, "z": 1 })
evaluates to{ 1: [ "z", "x" ], 2: ["y"] }
The following session in the REPL should be possible:
hi> count("Hello World").o
2
hi> invert(count("big blue bag"))
{ 1: [ "u", "l", "i", "e", "a" ], 2: [ "g", " " ], 3: ["b"] }
hi> fold(add, values(count("Hello, World!")))
13