bergamot-elm/src/Main.elm

381 lines
13 KiB
Elm
Raw Normal View History

port module Main exposing (main)
2023-11-26 11:43:38 -08:00
import Browser
import Html exposing (Html)
import Html.Events exposing (onInput, onClick)
import Html.Attributes exposing (type_, class, classList, value)
import Html.Lazy
2023-11-26 11:43:38 -08:00
import Bergamot.Syntax exposing (..)
import Bergamot.Search exposing (..)
import Bergamot.Rules exposing (..)
import Bergamot.Parser exposing (..)
2023-11-26 14:34:52 -08:00
import Bergamot.Latex exposing (..)
import Bergamot.Utils exposing (..)
2023-11-26 15:54:01 -08:00
import Json.Encode
import Json.Decode exposing (string, field, list, oneOf, succeed, fail)
2023-11-26 11:43:38 -08:00
import Maybe
import Tuple
type Tab
= Editor
| MetaEditor
| Rendered
tabEq : Tab -> Tab -> Bool
tabEq t1 t2 =
case (t1, t2) of
(Editor, Editor) -> True
(MetaEditor, MetaEditor) -> True
(Rendered, Rendered) -> True
_ -> False
type EditMode
= Query
| Custom String
type ResultMode
= Conclusion
| Tree
type DesugaringState
= Requested
| Succeeded String
| Failed String
resultModeEq : ResultMode -> ResultMode -> Bool
resultModeEq rm1 rm2 =
case (rm1, rm2) of
(Conclusion, Conclusion) -> True
(Tree, Tree) -> True
_ -> False
type alias Model =
{ program : String
-- ^ The Bergamot rules to execute a search against
, renderProgram: String
-- ^ The Bergamot render rules to apply when generating LaTeX
, tab : Tab
-- ^ The current tab (editor, redner rule editor, rendered)
, input : String
-- ^ The string the user has in the input box
, desugaredQuery : DesugaringState
-- ^ The Bergamot query corresponding to input, if we're in custom mode.
, inputModes : List (String, EditMode)
-- ^ A List of input modes that can be used with the input box.
, inputMode : Int
-- ^ The index of the currently selected input mode.
, resultMode : ResultMode
-- ^ How the result should be displayed (judgement or proof tree)
}
type alias Flags =
{ renderRules : String
, rules : String
, input : String
, inputModes : Json.Decode.Value
}
2023-11-26 11:58:20 -08:00
type Msg
= SetProgram String
| SetRenderProgram String
| SetInput String
| SetTab Tab
| SetInputMode Int
| SetResultMode ResultMode
| SetDesugaredQuery { input: String, result: Json.Decode.Value }
2023-11-26 11:43:38 -08:00
decodeRenderResult : Json.Decode.Value -> Result String String
decodeRenderResult val =
let
succeedDecoder = field "query" string |> Json.Decode.map Ok
failDecoder = field "error" string |> Json.Decode.map Err
decoder = Json.Decode.oneOf [ succeedDecoder, failDecoder ]
in
case Json.Decode.decodeValue decoder val of
Ok v -> v
Err _ -> Err "Unable to convert input into query"
decodeInputModes : Json.Decode.Value -> List (String, EditMode)
decodeInputModes val =
let
exactString s1 v =
string
|> Json.Decode.andThen (\s2 ->
if s1 == s2
then succeed v
else Json.Decode.fail "did not match expected string")
editModeDecoder = oneOf
[ exactString "query" Query
, field "custom" string |> Json.Decode.map Custom
]
decoder = Json.Decode.keyValuePairs editModeDecoder
in
case Json.Decode.decodeValue decoder val of
Ok l -> l
Err _ -> [("Query", Query)]
-- Convert the user-entered string 'input' using custom query mode 'mode'
port convertInput : { mode : String, input : String } -> Cmd msg
-- Invoked when user code finishes converting 'input' into a Bergamot query
port receiveConverted : ({ input : String, result : Json.Decode.Value } -> msg) -> Sub msg
requestDesugaring : Int -> List (String, EditMode) -> String -> Cmd Msg
requestDesugaring inputMode inputModes input =
case getEditMode inputMode inputModes of
Ok (Custom modeType) ->
convertInput { mode = modeType, input = input }
_ -> Cmd.none
2023-11-26 11:43:38 -08:00
init : Flags -> (Model, Cmd Msg)
init fs =
let
inputModes = decodeInputModes fs.inputModes
inputMode = 0
in
( { program = fs.rules
, renderProgram = fs.renderRules
, input = fs.input
, desugaredQuery = Requested
, tab = Rendered
, inputModes = inputModes
, inputMode = 0
, resultMode = Conclusion
}
, requestDesugaring inputMode inputModes fs.input
)
viewSection : String -> Html Msg -> Html Msg
viewSection name content =
Html.div [ class "bergamot-section" ] [ Html.em [ class "bergamot-section-heading" ] [ Html.text name ], content ]
2023-11-26 14:34:52 -08:00
viewTab : Tab -> Html Msg -> Html Msg -> Html Msg -> Html Msg
viewTab tab editor metaEditor rendered =
case tab of
Editor -> editor
MetaEditor -> metaEditor
Rendered -> rendered
viewSelector : (Int -> a -> b -> Bool) -> (Int -> a -> Msg) -> b -> List (a, String) -> Html Msg
viewSelector eq mkMsg mode modeNames =
Html.div [ class "bergamot-selector" ] <|
List.indexedMap (\idx (modep, name) ->
Html.button
[ classList [("active", eq idx modep mode)]
, onClick (mkMsg idx modep)
] [ Html.text name ]) modeNames
viewTabSelector : Tab -> List (Tab, String) -> Html Msg
viewTabSelector = viewSelector (\_ -> tabEq) (\_ -> SetTab)
viewInputModeSelector : Int -> List (EditMode, String) -> Html Msg
viewInputModeSelector = viewSelector (\i _ idx -> i == idx) (\idx _ -> SetInputMode idx)
viewResultModeSelector : ResultMode -> List (ResultMode, String) -> Html Msg
viewResultModeSelector = viewSelector (\_ -> resultModeEq) (\_ -> SetResultMode)
viewRule : RuleEnv -> Rule -> Maybe (Html Msg)
viewRule env r = renderRuleViaRules env r
|> Maybe.map latex
viewRuleSection : RuleEnv -> Section -> Maybe (Html Msg)
viewRuleSection env sec =
List.map (viewRule env) sec.rules
|> sequenceMaybes
|> Maybe.map (\rs ->
Html.div [ class "bergamot-rule-section" ]
[ Html.div [ class "bergamot-rule-list" ] rs
, Html.p [class "bergamot-rule-section-name"] [Html.text (sec.name)]
])
viewRules : String -> String -> Html Msg
viewRules renderProgs progs =
Html.div [ class "bergamot-section-list" ] <|
case progAndRenderProg progs renderProgs of
Ok (prog, renderProg) -> List.filterMap (viewRuleSection renderProg) prog.sections
_ -> []
type Error
= BadQuery
| NoConversionResults
| BadInputProg
| BadProg
| BadRenderProg
| FailedRender String
| InvalidInputMode
| Silent
combineTwoResults : (a -> b -> Result Error c) -> Result Error a -> Result Error b -> Result Error c
combineTwoResults f ra rb =
case ra of
Err _ -> Err Silent
Ok a ->
case rb of
Err _ -> Err Silent
Ok b -> f a b
errorToString : Error -> String
errorToString err =
case err of
BadQuery -> "Unable to parse input query"
NoConversionResults -> "Failed to convert object language term to proof goal"
BadInputProg -> "Unable to parse conversion rules from object language to proof goals"
BadProg -> "Unable to parse rules"
BadRenderProg -> "Unable to parse rendering rules"
FailedRender s -> s
InvalidInputMode -> "Invalid edit mode specified"
Silent -> ""
viewError : Error -> Html Msg
viewError e = Html.div [ class "bergamot-error" ] [ Html.text <| errorToString e ]
viewIfError : Result Error a -> List (Html Msg)
viewIfError r =
case r of
Err Silent -> []
Err e -> [ viewError e ]
_ -> []
viewOrError : Result Error (Html Msg) -> Html Msg
viewOrError r =
case r of
Err Silent -> Html.div [] []
Err e -> Html.div [] [ viewError e ] -- The div wrapper is needed because Elm has a bug?
Ok html -> html
getEditMode : Int -> List (String, EditMode) -> Result Error EditMode
getEditMode i l =
case l of
[] -> Err InvalidInputMode
((_, editMode) :: xs) ->
if i == 0 then Ok editMode else getEditMode (i - 1) xs
proofGoal : EditMode -> String -> DesugaringState -> Result Error (Term Metavariable)
proofGoal editMode input desugaredQuery =
if input == ""
then Err Silent
else
case editMode of
Query ->
case run topLevelTerm input of
Nothing -> Err BadQuery
Just query -> Ok query
Custom _ ->
case desugaredQuery of
Requested -> Err Silent
Succeeded querys ->
case run topLevelTerm querys of
Nothing -> Err BadQuery
Just query -> Ok query
Failed s -> Err (FailedRender s)
progAndRenderProg : String -> String -> Result Error (RuleEnv, RuleEnv)
progAndRenderProg progs renderProgs =
case (run program progs, run program renderProgs) of
(Just prog, Just renderProg) -> Ok (prog, renderProg)
(Nothing, _) -> Err BadProg
(_, Nothing) -> Err BadRenderProg
renderProofTree : ResultMode -> ProofTree -> RuleEnv -> Result Error (Html Msg)
renderProofTree resultMode (MkProofTree node) renderProg =
let
maybeLatexString =
case resultMode of
Conclusion -> renderTermViaRules renderProg (quoteVariables node.conclusion)
Tree -> renderTreeViaRules renderProg (MkProofTree node)
in
List.filterMap (Maybe.map latex) [maybeLatexString]
|> Html.div [ class "bergamot-proof-tree" ]
|> Ok
viewProofTree : ResultMode -> Term Metavariable -> (RuleEnv, RuleEnv) -> Result Error (Html Msg)
viewProofTree resultMode query (prog, renderProg) =
case single prog (prove query |> Bergamot.Rules.andThen reifyProofTree) of
Just proofTree -> renderProofTree resultMode proofTree renderProg
2023-12-29 00:07:14 -08:00
Nothing -> Ok <| Html.div [ class "bergamot-no-proofs" ]
[ Html.text "No applicable rules to prove input" ]
2023-11-26 11:43:38 -08:00
view : Model -> Html Msg
view m =
let
mode = getEditMode m.inputMode m.inputModes
termOrErr = mode |> Result.andThen (\editMode -> proofGoal editMode m.input m.desugaredQuery)
progsOrErr = progAndRenderProg m.program m.renderProgram
proofTreeOrErr = combineTwoResults (viewProofTree m.resultMode) termOrErr progsOrErr
in
Html.div [ class "bergamot-root" ]
[ viewSection "Input" <| Html.div [] <|
[ viewInputModeSelector m.inputMode <|
List.map (\(a, b) -> (b, a)) m.inputModes
, Html.input [ type_ "text", onInput SetInput, value m.input ] []
] ++
viewIfError termOrErr
, viewSection "Result" <| Html.div[]
[ viewResultModeSelector m.resultMode [(Conclusion, "Conclusion Only"), (Tree, "Full Proof Tree")]
, viewOrError proofTreeOrErr
]
, viewSection "Rules" <| Html.div [] <|
[ viewTabSelector m.tab [(Rendered, "Rendered"), (Editor, "Editor"), (MetaEditor, "Presentation Rules")]
, viewTab m.tab
(Html.textarea [ onInput SetProgram, value m.program ] [ ])
(Html.textarea [ onInput SetRenderProgram, value m.renderProgram ] [ ])
(Html.Lazy.lazy2 viewRules m.renderProgram m.program)
] ++
viewIfError progsOrErr
]
2023-11-26 11:43:38 -08:00
update : Msg -> Model -> (Model, Cmd Msg)
update msg m =
case msg of
2023-11-26 11:58:20 -08:00
SetProgram prog -> ({ m | program = prog }, Cmd.none)
SetRenderProgram prog -> ({ m | renderProgram = prog }, Cmd.none)
SetInput input ->
( { m | input = input, desugaredQuery = Requested }
, requestDesugaring m.inputMode m.inputModes input
)
SetTab tab -> ({ m | tab = tab }, Cmd.none)
SetInputMode mode ->
( { m | inputMode = mode, desugaredQuery = Requested }
, requestDesugaring mode m.inputModes m.input
)
SetResultMode mode -> ({ m | resultMode = mode }, Cmd.none)
SetDesugaredQuery data ->
({ m | desugaredQuery =
if m.input == data.input
then
case decodeRenderResult data.result of
Ok q -> Succeeded q
Err e -> Failed e
else m.desugaredQuery
}
, Cmd.none
)
2023-11-26 11:43:38 -08:00
subscriptions : Model -> Sub Msg
subscriptions _ = receiveConverted SetDesugaredQuery
2023-11-26 11:43:38 -08:00
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
2023-11-26 15:54:01 -08:00
-- Latex support:
-- Based on https://stackoverflow.com/questions/75492820/embedding-mathematical-equations-in-an-elm-spa
latex : String -> Html Msg
latex expr =
Html.node "katex-expression"
[ Html.Attributes.attribute "expression" expr
, Html.Attributes.attribute "katex-options" (Json.Encode.encode 0 options)
]
[]
options : Json.Encode.Value
options =
Json.Encode.object
[ ( "displayMode", Json.Encode.bool True )
]