385 lines
10 KiB
Elm
385 lines
10 KiB
Elm
import Html exposing (Html, div, text, textarea, h1, h2, p)
|
|
import Html.Attributes exposing (class, classList)
|
|
import Html.Events exposing (onInput)
|
|
import Browser exposing (element)
|
|
import Canvas exposing (Shape)
|
|
import Dict exposing (Dict)
|
|
import Parser exposing (Parser, Problem(..), DeadEnd, (|.), (|=), variable, succeed, symbol, int, spaces, map, oneOf, lazy, sequence, Trailing(..), keyword, run, backtrackable)
|
|
import Color
|
|
import Canvas exposing (Point, path, lineTo, circle, shapes, stroke, lineWidth, fill, rect)
|
|
import Set
|
|
|
|
{-
|
|
======= Syntax Definitions =======
|
|
-}
|
|
type LogoPenMode = Up | Down
|
|
type LogoExpr = Variable String
|
|
| Number Int
|
|
| Sum LogoExpr LogoExpr
|
|
type LogoCmd = Pen LogoPenMode
|
|
| Move LogoExpr LogoExpr
|
|
| Define String (List String) LogoProg
|
|
| Call String (List LogoExpr)
|
|
type alias LogoProg = List LogoCmd
|
|
|
|
{-
|
|
======= Semantics Definitions ====
|
|
-}
|
|
|
|
type alias Coord = (Int, Int)
|
|
type alias Line = (Coord, Coord)
|
|
type alias Function = (List String, LogoProg)
|
|
|
|
type Scope = Child Scope (Dict String Function) (Dict String Int) | End
|
|
type alias Board = List Line
|
|
type alias State =
|
|
{ penMode : LogoPenMode
|
|
, pos : Coord
|
|
, scope : Scope
|
|
, board : Board
|
|
}
|
|
|
|
type alias Step a = State -> Result String (a, State)
|
|
|
|
{-
|
|
======= Type Definitions =======
|
|
-}
|
|
|
|
type alias Flags =
|
|
{ initialText : String
|
|
}
|
|
|
|
type alias Model =
|
|
{ programText : String
|
|
}
|
|
|
|
type Msg
|
|
= UpdateText String
|
|
|
|
{-
|
|
======= Syntax Code
|
|
-}
|
|
|
|
parseInt : Parser LogoExpr
|
|
parseInt = map Number int
|
|
|
|
parseVarName : Parser String
|
|
parseVarName = variable
|
|
{ start = Char.isAlphaNum
|
|
, inner = Char.isAlphaNum
|
|
, reserved = Set.fromList [ "move", "pen", "define", "call" ]
|
|
}
|
|
|
|
parseVar : Parser LogoExpr
|
|
parseVar = map Variable parseVarName
|
|
|
|
parseTerm : Parser LogoExpr
|
|
parseTerm = oneOf [ parseInt, parseVar ]
|
|
|
|
parseSum : Parser LogoExpr
|
|
parseSum = succeed Sum
|
|
|= parseTerm
|
|
|. spaces
|
|
|. symbol "+"
|
|
|. spaces
|
|
|= parseExpr
|
|
|
|
parseExpr : Parser LogoExpr
|
|
parseExpr = oneOf [ lazy (\_ -> backtrackable parseSum), parseTerm ]
|
|
|
|
parseProg : Parser LogoProg
|
|
parseProg = sequence
|
|
{ start = ""
|
|
, separator = ";"
|
|
, end = ""
|
|
, spaces = spaces
|
|
, item = lazy (\_ -> parseCmd)
|
|
, trailing = Mandatory
|
|
}
|
|
|
|
parseCmd : Parser LogoCmd
|
|
parseCmd = oneOf [ parseDefine, parseCall, parseMove, parsePen ]
|
|
|
|
parseDefine : Parser LogoCmd
|
|
parseDefine = succeed Define
|
|
|. keyword "define"
|
|
|. spaces
|
|
|= parseVarName
|
|
|. spaces
|
|
|= sequence
|
|
{ start = "("
|
|
, separator = ","
|
|
, end = ")"
|
|
, spaces = spaces
|
|
, item = parseVarName
|
|
, trailing = Forbidden
|
|
}
|
|
|. spaces
|
|
|. symbol "{"
|
|
|. spaces
|
|
|= parseProg
|
|
|. spaces
|
|
|. symbol "}"
|
|
|
|
parseCall : Parser LogoCmd
|
|
parseCall = succeed Call
|
|
|. keyword "call"
|
|
|. spaces
|
|
|= parseVarName
|
|
|. spaces
|
|
|= sequence
|
|
{ start = "("
|
|
, separator = ","
|
|
, end = ")"
|
|
, spaces = spaces
|
|
, item = parseExpr
|
|
, trailing = Forbidden
|
|
}
|
|
|
|
parsePenMode : Parser LogoPenMode
|
|
parsePenMode = oneOf
|
|
[ succeed Up |. keyword "up"
|
|
, succeed Down |. keyword "down"
|
|
]
|
|
|
|
parsePen : Parser LogoCmd
|
|
parsePen = succeed Pen
|
|
|. keyword "pen"
|
|
|. spaces
|
|
|= parsePenMode
|
|
|
|
parseMove : Parser LogoCmd
|
|
parseMove = succeed Move
|
|
|. keyword "move"
|
|
|. spaces
|
|
|. symbol "("
|
|
|= parseExpr
|
|
|. spaces
|
|
|. symbol ","
|
|
|. spaces
|
|
|= parseExpr
|
|
|. symbol ")"
|
|
|
|
{-
|
|
======= Semantics Code =======
|
|
-}
|
|
|
|
initialScope : Scope
|
|
initialScope = Child End Dict.empty Dict.empty
|
|
|
|
setScopeFunction : String -> Function -> Scope -> Scope
|
|
setScopeFunction s f scope = case scope of
|
|
End -> setScopeFunction s f initialScope
|
|
Child p fs vs -> Child p (Dict.insert s f fs) vs
|
|
|
|
setScopeVariable : String -> Int -> Scope -> Scope
|
|
setScopeVariable s v scope = case scope of
|
|
End -> setScopeVariable s v initialScope
|
|
Child p fs vs -> Child p fs (Dict.insert s v vs)
|
|
|
|
scopeFunction : String -> Scope -> Maybe Function
|
|
scopeFunction s scope = case scope of
|
|
End -> Nothing
|
|
Child p fs _ -> case Dict.get s fs of
|
|
Just f -> Just f
|
|
Nothing -> scopeFunction s p
|
|
|
|
scopeVariable : String -> Scope -> Maybe Int
|
|
scopeVariable s scope = case scope of
|
|
End -> Nothing
|
|
Child p _ vs -> case Dict.get s vs of
|
|
Just i -> Just i
|
|
Nothing -> scopeVariable s p
|
|
|
|
initialState : State
|
|
initialState =
|
|
{ penMode = Up
|
|
, pos = (0, 0)
|
|
, scope = initialScope
|
|
, board = []
|
|
}
|
|
|
|
unit : a -> Step a
|
|
unit a state = Ok (a, state)
|
|
|
|
bind : Step a -> (a -> Step b) -> Step b
|
|
bind sa f state = case sa state of
|
|
Ok (a, newState) -> f a newState
|
|
Err e -> Err e
|
|
|
|
rbind : (a -> Step b) -> Step a -> Step b
|
|
rbind f sa = bind sa f
|
|
|
|
updatePosition : Coord -> Step ()
|
|
updatePosition c state =
|
|
let
|
|
newBoard = case state.penMode of
|
|
Up -> state.board
|
|
Down -> (state.pos, c)::state.board
|
|
in
|
|
Ok ((), { state | pos = c, board = newBoard })
|
|
|
|
registerFunction : String -> Function -> Step ()
|
|
registerFunction s f state = Ok ((), { state | scope = setScopeFunction s f state.scope })
|
|
|
|
registerVariable : String -> Int -> Step ()
|
|
registerVariable s v state = Ok ((), { state | scope = setScopeVariable s v state.scope })
|
|
|
|
setPenMode : LogoPenMode -> Step ()
|
|
setPenMode m state = Ok ((), { state | penMode = m})
|
|
|
|
upScope : Step ()
|
|
upScope state = Ok ((), { state | scope = Child state.scope Dict.empty Dict.empty })
|
|
|
|
downScope : Step ()
|
|
downScope state =
|
|
let
|
|
newScope = case state.scope of
|
|
End -> End
|
|
Child p _ _ -> p
|
|
in
|
|
Ok ((), { state | scope = newScope })
|
|
|
|
lookupFunction : String -> Step Function
|
|
lookupFunction s state = case scopeFunction s state.scope of
|
|
Just f -> Ok (f, state)
|
|
Nothing -> Err ("No function with name " ++ s)
|
|
|
|
lookupVariable : String -> Step Int
|
|
lookupVariable s state = case scopeVariable s state.scope of
|
|
Just i -> Ok (i, state)
|
|
Nothing -> Err ("No variable with name " ++ s)
|
|
|
|
evaluateExp : LogoExpr -> Step Int
|
|
evaluateExp le = case le of
|
|
Number i -> unit i
|
|
Variable v -> lookupVariable v
|
|
Sum l r -> evaluateExp l |> rbind (\vl -> evaluateExp r |> rbind (\vr -> unit (vl + vr)))
|
|
|
|
evaluateAllExps : List LogoExpr -> Step (List Int)
|
|
evaluateAllExps = List.foldr (\x acc -> acc |> rbind (\vs -> evaluateExp x |> rbind (\v -> unit (v::vs)))) (unit [])
|
|
|
|
registerAllVariables : List (String, Int) -> Step ()
|
|
registerAllVariables = List.foldl (\(s, i) m -> bind m (\_ -> registerVariable s i)) (unit ())
|
|
|
|
zip : List a -> List b -> List (a, b)
|
|
zip la lb = case (la, lb) of
|
|
(a::lat, b::lbt) -> (a, b)::zip lat lbt
|
|
_ -> []
|
|
|
|
evaluateCmd : LogoCmd -> Step ()
|
|
evaluateCmd c = case c of
|
|
Move l r -> evaluateExp l |> rbind (\x -> evaluateExp r |> rbind (\y -> updatePosition (x, y)))
|
|
Define n xs prg -> registerFunction n (xs, prg)
|
|
Pen mode -> setPenMode mode
|
|
Call n args -> lookupFunction n
|
|
|> rbind (\(ns, prg) -> evaluateAllExps args
|
|
|> rbind (\vs -> upScope
|
|
|> rbind (\_ -> registerAllVariables (zip ns vs)
|
|
|> rbind (\_ -> evaluateAll prg
|
|
|> rbind (\_ -> downScope)))))
|
|
|
|
evaluateAll : LogoProg -> Step ()
|
|
evaluateAll prg = List.foldl rbind (unit ()) <| List.map (\c _ -> evaluateCmd c) prg
|
|
|
|
{-
|
|
======= Drawing Stuff =====
|
|
-}
|
|
transformCoord : Coord -> Point
|
|
transformCoord (x, y) = (toFloat x * 25, 500 - toFloat y * 25)
|
|
|
|
lineShape : Line -> Shape
|
|
lineShape (c1, c2) = path (transformCoord c1) [ lineTo (transformCoord c2) ]
|
|
|
|
pointerShape : Coord -> Shape
|
|
pointerShape c = circle (transformCoord c) 5
|
|
|
|
allShapes : State -> List Shape
|
|
allShapes s = pointerShape s.pos :: List.map lineShape s.board
|
|
|
|
canvas : State -> Html Msg
|
|
canvas state = Canvas.toHtml (500, 500) [ classList [ ("centered", True), ("shadow", True) ] ]
|
|
[ shapes [ fill Color.white ] [ rect (0, 0) 500 500 ]
|
|
, shapes [ stroke Color.blue, lineWidth 5] (allShapes state)
|
|
]
|
|
|
|
{-
|
|
===== Rendering Stuff
|
|
-}
|
|
|
|
humanProblemString : Problem -> String
|
|
humanProblemString p = case p of
|
|
Expecting s -> "Expecting " ++ s
|
|
ExpectingInt -> "Expecting decimal integer"
|
|
ExpectingHex -> "Expecting hexadecimal integer"
|
|
ExpectingOctal -> "Expecting octal integer"
|
|
ExpectingBinary -> "Expecting binary integer"
|
|
ExpectingFloat -> "Expecting a floating pointer number"
|
|
ExpectingNumber -> "Expecting a number"
|
|
ExpectingVariable -> "Expected variable name"
|
|
ExpectingSymbol s -> "Expected symbol \"" ++ s ++ "\""
|
|
ExpectingKeyword s -> "Expecting keyword " ++ s
|
|
ExpectingEnd -> "Expecting end of string"
|
|
UnexpectedChar -> "Unexpected character"
|
|
Problem s -> s
|
|
BadRepeat -> "Bad repetition"
|
|
|
|
humanDeadEndString : DeadEnd -> String
|
|
humanDeadEndString { row, col, problem } =
|
|
"An parse error occured on line "
|
|
++ String.fromInt row
|
|
++ ": " ++ humanProblemString problem
|
|
|
|
humanParseError : List DeadEnd -> String
|
|
humanParseError = Maybe.withDefault "" << Maybe.map humanDeadEndString << List.head
|
|
|
|
{-
|
|
======= Elm Architecture =======
|
|
-}
|
|
|
|
init : Flags -> (Model, Cmd Msg)
|
|
init fs =
|
|
let
|
|
model =
|
|
{ programText = fs.initialText
|
|
}
|
|
cmd = Cmd.none
|
|
in
|
|
(model, cmd)
|
|
|
|
view : Model -> Html Msg
|
|
view m =
|
|
let
|
|
evalResult = Result.map (\(_, s) -> s)
|
|
<| Result.andThen (\prg -> evaluateAll prg initialState)
|
|
<| Result.mapError humanParseError (run parseProg m.programText)
|
|
toRender = Result.withDefault initialState evalResult
|
|
error = case evalResult of
|
|
Err e -> [ p [ classList [ ("centered", True), ("error", True), ("shadow", True) ] ] [ text e ] ]
|
|
Ok _ -> []
|
|
in
|
|
div []
|
|
[ h1 [] [ text "MiniLogo Functional Evaluator" ]
|
|
, div [ class "logo-container" ]
|
|
[ div [ class "logo-pane" ] <|
|
|
[ h2 [] [ text "MiniLogo" ]
|
|
, textarea [ onInput UpdateText, classList [ ("centered", True), ("shadow", True) ] ] [ text m.programText ]
|
|
] ++ error
|
|
, div [ class "logo-pane" ] [ h2 [] [ text "Output" ], canvas toRender ]
|
|
]
|
|
]
|
|
|
|
update : Msg -> Model -> (Model, Cmd Msg)
|
|
update msg m = case msg of
|
|
UpdateText s -> ({ m | programText = s }, Cmd.none)
|
|
|
|
subscriptions : Model -> Sub Msg
|
|
subscriptions m = Sub.none
|
|
|
|
main = element
|
|
{ init = init
|
|
, view = view
|
|
, update = update
|
|
, subscriptions = subscriptions
|
|
}
|