Initial commit, we have a basic UI that lets you edit a toy-datalog program and conjunctive query and see results live.
This commit is contained in:
commit
f11d74147d
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
.attr-cache
|
||||
.cabal-sandbox
|
||||
*.hi
|
||||
*.o
|
||||
cabal.project.local
|
||||
cabal.sandbox.config
|
||||
ctags
|
||||
dist-newstyle/
|
||||
dist/
|
||||
ghcid-output.txt
|
||||
profile/
|
||||
result
|
||||
result-*
|
||||
tags
|
||||
TAGS
|
||||
static.out
|
||||
2
.obelisk/impl/default.nix
Normal file
2
.obelisk/impl/default.nix
Normal file
@ -0,0 +1,2 @@
|
||||
# DO NOT HAND-EDIT THIS FILE
|
||||
import (import ./thunk.nix)
|
||||
8
.obelisk/impl/github.json
Normal file
8
.obelisk/impl/github.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"owner": "obsidiansystems",
|
||||
"repo": "obelisk",
|
||||
"branch": "develop",
|
||||
"private": false,
|
||||
"rev": "c0767818c990f081796cfb4017c5931d75fa4803",
|
||||
"sha256": "0v16c1z09llramddq09ra8psh1mxxp7yvh62df84d1qq6qcgkmny"
|
||||
}
|
||||
12
.obelisk/impl/thunk.nix
Normal file
12
.obelisk/impl/thunk.nix
Normal file
@ -0,0 +1,12 @@
|
||||
# DO NOT HAND-EDIT THIS FILE
|
||||
let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
|
||||
if !fetchSubmodules && !private then builtins.fetchTarball {
|
||||
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
|
||||
} else (import (builtins.fetchTarball {
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/3aad50c30c826430b0270fcf8264c8c41b005403.tar.gz";
|
||||
sha256 = "0xwqsf08sywd23x0xvw4c4ghq0l28w2ki22h0bdn766i16z9q2gr";
|
||||
}) {}).fetchFromGitHub {
|
||||
inherit owner repo rev sha256 fetchSubmodules private;
|
||||
};
|
||||
json = builtins.fromJSON (builtins.readFile ./github.json);
|
||||
in fetch json
|
||||
43
backend/backend.cabal
Normal file
43
backend/backend.cabal
Normal file
@ -0,0 +1,43 @@
|
||||
name: backend
|
||||
version: 0.1
|
||||
cabal-version: >= 1.8
|
||||
build-type: Simple
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
if impl(ghcjs)
|
||||
buildable: False
|
||||
build-depends: base
|
||||
, common
|
||||
, frontend
|
||||
, obelisk-backend
|
||||
, obelisk-route
|
||||
exposed-modules:
|
||||
Backend
|
||||
ghc-options: -Wall -O -fno-show-valid-hole-fits
|
||||
-- unsafe code
|
||||
-Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields
|
||||
-- unneeded code
|
||||
-Widentities -Wredundant-constraints
|
||||
if impl(ghc >= 8.8)
|
||||
ghc-options:
|
||||
-Wmissing-deriving-strategies
|
||||
|
||||
executable backend
|
||||
main-is: main.hs
|
||||
hs-source-dirs: src-bin
|
||||
ghc-options: -Wall -O -fno-show-valid-hole-fits -threaded
|
||||
-- unsafe code
|
||||
-Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields
|
||||
-- unneeded code
|
||||
-Widentities -Wredundant-constraints
|
||||
if impl(ghc >= 8.8)
|
||||
ghc-options:
|
||||
-Wmissing-deriving-strategies
|
||||
if impl(ghcjs)
|
||||
buildable: False
|
||||
build-depends: base
|
||||
, backend
|
||||
, common
|
||||
, frontend
|
||||
, obelisk-backend
|
||||
1
backend/frontend.jsexe
Symbolic link
1
backend/frontend.jsexe
Symbolic link
@ -0,0 +1 @@
|
||||
../frontend-js/bin/frontend.jsexe
|
||||
1
backend/frontendJs/frontend.jsexe
Symbolic link
1
backend/frontendJs/frontend.jsexe
Symbolic link
@ -0,0 +1 @@
|
||||
../../frontend-js/bin/frontend.jsexe
|
||||
6
backend/src-bin/main.hs
Normal file
6
backend/src-bin/main.hs
Normal file
@ -0,0 +1,6 @@
|
||||
import Backend
|
||||
import Frontend
|
||||
import Obelisk.Backend
|
||||
|
||||
main :: IO ()
|
||||
main = runBackend backend frontend
|
||||
10
backend/src/Backend.hs
Normal file
10
backend/src/Backend.hs
Normal file
@ -0,0 +1,10 @@
|
||||
module Backend where
|
||||
|
||||
import Common.Route
|
||||
import Obelisk.Backend
|
||||
|
||||
backend :: Backend BackendRoute FrontendRoute
|
||||
backend = Backend
|
||||
{ _backend_run = \serve -> serve $ const $ return ()
|
||||
, _backend_routeEncoder = fullRouteEncoder
|
||||
}
|
||||
1
backend/static
Symbolic link
1
backend/static
Symbolic link
@ -0,0 +1 @@
|
||||
../static
|
||||
3
cabal.project
Normal file
3
cabal.project
Normal file
@ -0,0 +1,3 @@
|
||||
optional-packages:
|
||||
*
|
||||
write-ghc-environment-files: never
|
||||
21
common/common.cabal
Normal file
21
common/common.cabal
Normal file
@ -0,0 +1,21 @@
|
||||
name: common
|
||||
version: 0.1
|
||||
cabal-version: >= 1.2
|
||||
build-type: Simple
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
build-depends: base
|
||||
, obelisk-route
|
||||
, text
|
||||
exposed-modules:
|
||||
Common.Api
|
||||
Common.Route
|
||||
ghc-options: -Wall -O -fno-show-valid-hole-fits
|
||||
-- unsafe code
|
||||
-Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields
|
||||
-- unneeded code
|
||||
-Widentities -Wredundant-constraints
|
||||
if impl(ghc >= 8.8)
|
||||
ghc-options:
|
||||
-Wmissing-deriving-strategies
|
||||
4
common/src/Common/Api.hs
Normal file
4
common/src/Common/Api.hs
Normal file
@ -0,0 +1,4 @@
|
||||
module Common.Api where
|
||||
|
||||
commonStuff :: String
|
||||
commonStuff = "Here is a string defined in Common.Api"
|
||||
48
common/src/Common/Route.hs
Normal file
48
common/src/Common/Route.hs
Normal file
@ -0,0 +1,48 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE EmptyCase #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
module Common.Route where
|
||||
|
||||
{- -- You will probably want these imports for composing Encoders.
|
||||
import Prelude hiding (id, (.))
|
||||
import Control.Category
|
||||
-}
|
||||
|
||||
import Data.Text (Text)
|
||||
import Data.Functor.Identity
|
||||
|
||||
import Obelisk.Route
|
||||
import Obelisk.Route.TH
|
||||
|
||||
data BackendRoute :: * -> * where
|
||||
-- | Used to handle unparseable routes.
|
||||
BackendRoute_Missing :: BackendRoute ()
|
||||
-- You can define any routes that will be handled specially by the backend here.
|
||||
-- i.e. These do not serve the frontend, but do something different, such as serving static files.
|
||||
|
||||
data FrontendRoute :: * -> * where
|
||||
FrontendRoute_Main :: FrontendRoute ()
|
||||
-- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
|
||||
|
||||
fullRouteEncoder
|
||||
:: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
|
||||
fullRouteEncoder = mkFullRouteEncoder
|
||||
(FullRoute_Backend BackendRoute_Missing :/ ())
|
||||
(\case
|
||||
BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
|
||||
(\case
|
||||
FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
|
||||
|
||||
concat <$> mapM deriveRouteComponent
|
||||
[ ''BackendRoute
|
||||
, ''FrontendRoute
|
||||
]
|
||||
1
config/common/example
Normal file
1
config/common/example
Normal file
@ -0,0 +1 @@
|
||||
This string comes from config/common/example
|
||||
1
config/common/route
Normal file
1
config/common/route
Normal file
@ -0,0 +1 @@
|
||||
http://localhost:8000
|
||||
9
config/readme.md
Normal file
9
config/readme.md
Normal file
@ -0,0 +1,9 @@
|
||||
### Config
|
||||
|
||||
Obelisk projects should contain a config folder with the following subfolders: common, frontend, and backend.
|
||||
|
||||
Things that should never be transmitted to the frontend belong in backend/ (e.g., email credentials)
|
||||
|
||||
Frontend-only configuration belongs in frontend/.
|
||||
|
||||
Shared configuration files (e.g., the route config) belong in common/
|
||||
23
default.nix
Normal file
23
default.nix
Normal file
@ -0,0 +1,23 @@
|
||||
{ system ? builtins.currentSystem
|
||||
, obelisk ? import ./.obelisk/impl {
|
||||
inherit system;
|
||||
iosSdkVersion = "16.1";
|
||||
|
||||
# You must accept the Android Software Development Kit License Agreement at
|
||||
# https://developer.android.com/studio/terms in order to build Android apps.
|
||||
# Uncomment and set this to `true` to indicate your acceptance:
|
||||
# config.android_sdk.accept_license = false;
|
||||
|
||||
# In order to use Let's Encrypt for HTTPS deployments you must accept
|
||||
# their terms of service at https://letsencrypt.org/repository/.
|
||||
# Uncomment and set this to `true` to indicate your acceptance:
|
||||
# terms.security.acme.acceptTerms = false;
|
||||
}
|
||||
}:
|
||||
with obelisk;
|
||||
project ./. ({ ... }: {
|
||||
android.applicationId = "systems.obsidian.obelisk.examples.minimal";
|
||||
android.displayName = "Obelisk Minimal Example";
|
||||
ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal";
|
||||
ios.bundleName = "Obelisk Minimal Example";
|
||||
})
|
||||
2
dep/toy-datalog/default.nix
Normal file
2
dep/toy-datalog/default.nix
Normal file
@ -0,0 +1,2 @@
|
||||
# DO NOT HAND-EDIT THIS FILE
|
||||
import (import ./thunk.nix)
|
||||
8
dep/toy-datalog/git.json
Normal file
8
dep/toy-datalog/git.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"url": "https://code.obsidian.systems/cale/toy-datalog.git",
|
||||
"rev": "c09e07042b633c64021c10aac0258cc117d289cc",
|
||||
"sha256": "1fl7b5iqk02i97sdpsfpp698mj4gp9hkmzamycv9jacvq7zq1k5y",
|
||||
"private": false,
|
||||
"fetchSubmodules": false,
|
||||
"branch": "master"
|
||||
}
|
||||
17
dep/toy-datalog/thunk.nix
Normal file
17
dep/toy-datalog/thunk.nix
Normal file
@ -0,0 +1,17 @@
|
||||
# DO NOT HAND-EDIT THIS FILE
|
||||
let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:
|
||||
let realUrl = let firstChar = builtins.substring 0 1 url; in
|
||||
if firstChar == "/" then /. + url
|
||||
else if firstChar == "." then ./. + url
|
||||
else url;
|
||||
in if !fetchSubmodules && private then builtins.fetchGit {
|
||||
url = realUrl; inherit rev;
|
||||
${if branch == null then null else "ref"} = branch;
|
||||
} else (import (builtins.fetchTarball {
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/3aad50c30c826430b0270fcf8264c8c41b005403.tar.gz";
|
||||
sha256 = "0xwqsf08sywd23x0xvw4c4ghq0l28w2ki22h0bdn766i16z9q2gr";
|
||||
}) {}).fetchgit {
|
||||
url = realUrl; inherit rev sha256;
|
||||
};
|
||||
json = builtins.fromJSON (builtins.readFile ./git.json);
|
||||
in fetch json
|
||||
60
frontend/frontend.cabal
Normal file
60
frontend/frontend.cabal
Normal file
@ -0,0 +1,60 @@
|
||||
name: frontend
|
||||
version: 0.1
|
||||
cabal-version: >= 1.8
|
||||
build-type: Simple
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
build-depends: base
|
||||
, common
|
||||
, jsaddle
|
||||
, lens
|
||||
, obelisk-executable-config-lookup
|
||||
, obelisk-frontend
|
||||
, obelisk-generated-static
|
||||
, obelisk-route
|
||||
, reflex-dom-core
|
||||
, text
|
||||
, toy-datalog
|
||||
exposed-modules:
|
||||
Frontend
|
||||
default-extensions:
|
||||
ImportQualifiedPost,
|
||||
OverloadedStrings,
|
||||
FlexibleContexts
|
||||
ghc-options: -Wall -O -fno-show-valid-hole-fits
|
||||
-- unsafe code
|
||||
-Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields
|
||||
-- unneeded code
|
||||
-Widentities -Wredundant-constraints
|
||||
if impl(ghc >= 8.8)
|
||||
ghc-options:
|
||||
-Wmissing-deriving-strategies
|
||||
|
||||
executable frontend
|
||||
main-is: main.hs
|
||||
hs-source-dirs: src-bin
|
||||
build-depends: base
|
||||
, common
|
||||
, frontend
|
||||
, obelisk-frontend
|
||||
, obelisk-route
|
||||
, reflex-dom
|
||||
, toy-datalog
|
||||
default-extensions:
|
||||
ImportQualifiedPost,
|
||||
OverloadedStrings,
|
||||
FlexibleContexts
|
||||
ghc-options: -Wall -O -fno-show-valid-hole-fits -threaded
|
||||
-- unsafe code
|
||||
-Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields
|
||||
-- unneeded code
|
||||
-Widentities -Wredundant-constraints
|
||||
if impl(ghc >= 8.8)
|
||||
ghc-options:
|
||||
-Wmissing-deriving-strategies
|
||||
if impl(ghcjs)
|
||||
ghc-options: -dedupe
|
||||
cpp-options: -DGHCJS_BROWSER
|
||||
if os(darwin)
|
||||
ghc-options: -dynamic
|
||||
10
frontend/src-bin/main.hs
Normal file
10
frontend/src-bin/main.hs
Normal file
@ -0,0 +1,10 @@
|
||||
import Frontend
|
||||
import Common.Route
|
||||
import Obelisk.Frontend
|
||||
import Obelisk.Route.Frontend
|
||||
import Reflex.Dom
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
let Right validFullEncoder = checkEncoder fullRouteEncoder
|
||||
run $ runFrontend validFullEncoder frontend
|
||||
86
frontend/src/Frontend.hs
Normal file
86
frontend/src/Frontend.hs
Normal file
@ -0,0 +1,86 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Frontend where
|
||||
|
||||
import Control.Monad
|
||||
import Data.Text (Text)
|
||||
import Data.Text qualified as T
|
||||
|
||||
import Obelisk.Frontend
|
||||
import Obelisk.Route
|
||||
import Obelisk.Generated.Static
|
||||
|
||||
import Reflex.Dom.Core
|
||||
|
||||
import Common.Route
|
||||
|
||||
import Datalog
|
||||
import Text.Megaparsec qualified as P
|
||||
import Data.Map (Map)
|
||||
import Data.Map qualified as Map
|
||||
|
||||
example :: Text
|
||||
example = T.unlines
|
||||
[ "odd(X,Y) :- r(X,Y)."
|
||||
, "odd(X,Y) :- even(X,Z), r(Z,Y)."
|
||||
, "even(X,Y) :- odd(X,Z), r(Z,Y)."
|
||||
, "r(0,1)."
|
||||
, "r(1,2)."
|
||||
, "r(2,3)."
|
||||
, "r(3,4)."
|
||||
, "r(4,5)."
|
||||
, "r(X,Y) :- r(Y,X)."
|
||||
]
|
||||
|
||||
frontend :: Frontend (R FrontendRoute)
|
||||
frontend = Frontend
|
||||
{ _frontend_head = do
|
||||
el "title" $ text "Toy Datalog"
|
||||
elAttr "script" ("type" =: "application/javascript" <> "src" =: $(static "lib.js")) blank
|
||||
elAttr "link" ("href" =: $(static "main.css") <> "type" =: "text/css" <> "rel" =: "stylesheet") blank
|
||||
, _frontend_body = do
|
||||
el "h1" $ text "Toy Datalog"
|
||||
divClass "main" $ do
|
||||
tp <- divClass "program" $ textAreaElement $ def & textAreaElementConfig_initialValue .~ example
|
||||
tq <- divClass "query" $ inputElement $ def & inputElementConfig_initialValue .~ "even(A,B)"
|
||||
let programD = ffor (value tp) $ \p -> P.parse parseProgram "<program>" p
|
||||
queryD = ffor (value tq) $ \q -> P.parse parseQuery "<query>" q
|
||||
dyn_ . ffor2 programD queryD $ \p q -> case (p,q) of
|
||||
(Left e, _) -> el "pre" $ text $ T.pack (P.errorBundlePretty e)
|
||||
(_, Left e) -> el "pre" $ text $ T.pack (P.errorBundlePretty e)
|
||||
(Right prog, Right query) -> case extendFixedpointDb =<< addProgram prog emptyDatabase of
|
||||
Left e -> evalErrorWidget e
|
||||
Right db -> case evalConjunction db query of
|
||||
Left e -> evalErrorWidget e
|
||||
Right bindings -> divClass "bindings" $ forM_ bindings $ \b -> do
|
||||
bindingWidget b
|
||||
return ()
|
||||
}
|
||||
|
||||
evalErrorWidget :: (DomBuilder t m) => EvalError -> m ()
|
||||
evalErrorWidget e = divClass "eval-error" . el "pre" . text . T.unlines $ case e of
|
||||
EvalError_RuleWrongArity rule wa ->
|
||||
[ "Arity error in rule:"
|
||||
, pretty rule
|
||||
, "Relation " <> pretty (_wrongArity_relation wa) <> " has arity " <> T.pack (show (_wrongArity_expected wa))
|
||||
, "but appears with arity " <> T.pack (show (_wrongArity_got wa)) <> "."
|
||||
]
|
||||
EvalError_AtomWrongArity a wa ->
|
||||
[ "Arity error in atom:"
|
||||
, pretty a
|
||||
, "Relation " <> pretty (_wrongArity_relation wa) <> " has arity " <> T.pack (show (_wrongArity_expected wa))
|
||||
, "but appears with arity " <> T.pack (show (_wrongArity_got wa)) <> "."
|
||||
]
|
||||
|
||||
bindingWidget :: (DomBuilder t m) => Map VarId ConId -> m ()
|
||||
bindingWidget b = do
|
||||
divClass "binding" $ do
|
||||
if Map.null b
|
||||
then text "True"
|
||||
else
|
||||
el "ul" $ forM_ (Map.toList b) $ \(VarId v, ConId c) ->
|
||||
el "li" $
|
||||
text (v <> " = " <> c)
|
||||
4
static/lib.js
Normal file
4
static/lib.js
Normal file
@ -0,0 +1,4 @@
|
||||
// Accessing the property via string literal prevents renaming by javascript minifiers which can cause FFI errors
|
||||
window['skeleton_lib'] = {
|
||||
log: txt => console.log('Received "' + txt + '" from FFI'),
|
||||
};
|
||||
63
static/main.css
Normal file
63
static/main.css
Normal file
@ -0,0 +1,63 @@
|
||||
body {
|
||||
background: #220044;
|
||||
color: wheat;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.program {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.program textarea {
|
||||
background: #110022;
|
||||
color: wheat;
|
||||
width: 90%;
|
||||
height: 30em;
|
||||
border: solid 1px wheat;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.query {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.query input {
|
||||
background: #110022;
|
||||
color: wheat;
|
||||
width: 90%;
|
||||
border: solid 1px wheat;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.query input :focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.bindings {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.binding {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #330055;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border: solid 1px wheat;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.binding ul {
|
||||
margin: 0px;
|
||||
padding-left: 1em;
|
||||
}
|
||||
BIN
static/obelisk.jpg
Normal file
BIN
static/obelisk.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Loading…
x
Reference in New Issue
Block a user