Skip to content

Commit

Permalink
TCO for user defined functions via recur
Browse files Browse the repository at this point in the history
  • Loading branch information
albertprz committed May 1, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 9a3bd19 commit b29d326
Showing 15 changed files with 145 additions and 65 deletions.
60 changes: 52 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -25,16 +25,14 @@ for use for enterprise, personal & educational purposes.

- [✔️] Persist user defined formulas, spreadsheet data, functions & operators in the browser local storage.

- Format automatically formulas when evaluated.

- Add the capability for a user to create, modify or delete global functions & operators through an auxiliary modal in the Explorer view.

- Enable the use of formulas for filtering & sorting rows.

- Include Opt-in automatic formatting for formulas when evaluated.

- Support Import & Export of spreadsheets from and to Excel and Google Sheets with the proper mapping of cell values & formulas.

- Expose customization options for navigation, keybindings, spreadsheet behaviour as well as display and themes configuration.

- Support authenticated persistence & retrieval from a catalog of private spreadsheets via a future backend.


@@ -67,23 +65,69 @@ $ npm run serve

## Keybindings

#### General

<center>

| Key Combination | Action |
| --------------- | ------ |
| `Control` + `J` | Next view |
| `Control` + `K` | Previous view |

</center>


#### Spreadsheet table

<center>

| Key Combination | Action |
| --------------- | ------ |
| &uarr; &darr; &larr; &rarr; | Cell navigation |
| `h` `j` `k` `l` | Cell navigation (Vim like) |
| `Shift` + &uarr; &darr; &larr; &rarr; | Cell selection |
| `Shift` + `Hover` | Cell selection |
| `Tab` | Go to next cell |
| `Shift` + `Tab` | Go to previous cell |
| `Enter` | Edit cell value |
| `Control` + `Enter` | Edit formula |
| `Shift` + `Navigation` | Cell selection |
| `Backspace` | Delete cell/s |
| `Control` + `G` | Go to cell |
| `Control` + `A` | Select all cells |
| `Control` + `C` | Copy cells/s |
| `Control` + `V` | Paste cells/s |
| `Control` + `X` | Cut cells/s |
| `Control` + `C` | Copy cell/s |
| `Control` + `V` | Paste cell/s |
| `Control` + `X` | Cut cell/s |

</center>

#### Formula Editor

<center>

| Key Combination | Action |
| --------------- | ------ |
| `Tab` | Switch out of editor |
| `Control` + `Enter` | Submit formula |
| `Enter` | Autocomplete with current suggestion
| &uarr; | Previous autocomplete suggestion |
| &darr; | Next autocomplete suggestion |
| `Shift` + `Tab` | Previous autocomplete suggestion |
| `Tab` | Next autocomplete suggestion |
| `Control` + `Click` | Go to function / module documentation |
| `Control` + `D` | Go to function / module documentation |

</center>

#### Function Explorer

<center>

| Key Combination | Action |
| --------------- | ------ |
| `Control` + `M` | Switch module |
| `Control` + `F` | Search for function |
| &uarr; &darr; | Previous / next function |
| `Shift` + `Tab` `Tab` | Previous / next function |

</center>

44 changes: 26 additions & 18 deletions lib/Prelude.pursh
Original file line number Diff line number Diff line change
@@ -94,29 +94,33 @@ def map (f: A -> B, xs: [A]): [B] =
// Maps a unary function over a collection
// >>> _ / 2, [1, 3, 5, 7]
// >>> _ ++ "x", ["a", "c", "c"]
switch (xs) {
| [] => []
| [ x, xs @ ... ] => f (x) +: map (f, xs)
go (f, xs, []) where {
| go (f, xs, acc) = switch (xs) {
| [] => acc
| [ xs @ ... , x ] => recur (f, xs, f (x) +: acc)
}
}

def filter (f: A -> Boolean, xs: [A]): [A] =
// Uses a predicate to filter a collection
// >>> x -> x % 2 == 0, [1, 2, 3, 4]
// >>> x -> length (x) > 2, ["abc", "f"]
switch (xs) {
| [] => []
| [ x, xs @ ... ] ? f(x) => x +: filter (f, xs)
? otherwise => filter (f, xs)
go (f, xs, []) where {
| go (f, xs, acc) = switch (xs) {
| [] => acc
| [ x, xs @ ... ] ? f(x) => recur (f, xs, x +: acc)
? otherwise => recur (f, xs, acc)
}
}

def reduce (f: B -> A -> B, start: B, xs: [A]): B =
// Uses a function and an initial value to accumulate over a collection
// >>> '+, 7, [1, 2, 3, 4]
// >>> '++, "hello", ["abc", "f"]
reduceRight (f, start, reverse (xs)) where {
| reduceRight (f, start, xs) = switch (xs) {
| [] => start
| [ x, xs @ ... ] => f (reduceRight (f, start, xs), x)
go (f, start, reverse (xs), start) where {
| go (f, start, xs, acc) = switch (xs) {
| [] => acc
| [ x, xs @ ... ] => recur (f, start, xs, f(acc, x))
}
}

@@ -132,19 +136,23 @@ def zipWith (f: A -> B -> C, xs: [A], ys: [B]): [C] =
// Maps a binary function over two collections
// >>> '-, [1, 2, 3], [5, 6, 7]
// >>> '++, ["a", "b"], ["c", "d"]
cond {
? isEmpty (xs) || isEmpty (ys) => []
? otherwise =>
f (head (xs), head (ys)) +: zipWith (f, tail (xs), tail (ys))
go (f, xs, ys, []) where {
| go (f, xs, ys, acc) = cond {
? isEmpty (xs) || isEmpty (ys) => acc
? otherwise => recur (f, tail (xs), tail (ys),
f (head (xs), head (ys)) +: acc)
}
}

def iterate (f: A -> A, start: A, n: Int): [A] =
// Generates a collection by applying a function n times with an initial value
// >>> _ * 2, 1, 6
// >>> _ ++ "x", "a", 3
cond {
? n == 0 => [start]
? otherwise => start +: iterate (f, f (start), n - 1)
go (f, start, n, []) where {
| go (f, start, n, acc) = cond {
? n == 0 => reverse (acc)
? otherwise => recur (f, f (start), n - 1, start +: acc)
}
}

def isEmpty (xs: [A]): Boolean =
1 change: 1 addition & 0 deletions spago.dhall
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@
, "safe-coerce"
, "strings"
, "stringutils"
, "tailrec"
, "tecton"
, "tecton-halogen"
, "transformers"
2 changes: 1 addition & 1 deletion src/AppStore.purs
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@ import Data.HashMap as HashMap
import Data.Set as Set
import Data.Set.NonEmpty as NonEmptySet
import Data.Tree.Zipper (fromTree)
import Effect.Console (log)
import Effect.Console as Logger
import Foreign (readArray, readString, unsafeToForeign)
import Foreign.Index ((!))
@@ -63,6 +62,7 @@ mkLocalContext store =
{ module': preludeModule
, localFnsMap: HashMap.empty
, argsMap: HashMap.empty
, fnInfo: Nothing
, scope: zero
, scopeLoc: fromTree $ mkLeaf zero
, lambdaCount: zero
1 change: 0 additions & 1 deletion src/Components/Editor/Suggestion.purs
Original file line number Diff line number Diff line change
@@ -11,7 +11,6 @@ import Bookhound.Parser (Parser, runParser)
import Bookhound.ParserCombinators (is)
import Bookhound.Parsers.Char (lower)
import Data.Array as Array
import Data.Generic.Rep (class Generic)
import Data.HashMap as HashMap
import Data.Set as Set
import Data.String.CodeUnits (indexOf', lastIndexOf')
6 changes: 2 additions & 4 deletions src/Components/Spreadsheet/Handler.purs
Original file line number Diff line number Diff line change
@@ -153,9 +153,7 @@ handleAction (KeyDown Space _) = do
handleAction (KeyDown Delete _) =
deleteCells

handleAction (KeyDown Shift ev) = prevent ev

handleAction (KeyDown Control ev) = withPrevent ev $
handleAction (KeyDown Shift ev) = withPrevent ev $
modify_ _ { selectionState = InProgressSelection }

handleAction (KeyDown (CharKeyCode 'A') ev)
@@ -176,7 +174,7 @@ handleAction (KeyDown (CharKeyCode 'G') ev)
handleAction (KeyDown _ _) =
pure unit

handleAction (KeyUp Control ev) = withPrevent ev $
handleAction (KeyUp Shift ev) = withPrevent ev $
modify_ _ { selectionState = NotStartedSelection }

handleAction (KeyUp _ _) =
27 changes: 24 additions & 3 deletions src/Evaluator/Common.purs
Original file line number Diff line number Diff line change
@@ -11,6 +11,9 @@ import App.SyntaxTree.FnDef (Associativity(..), BuiltinFnInfo, FnBody(..), FnDef
import App.SyntaxTree.Pattern (Pattern(..))
import Bookhound.Parser (runParser)
import Bookhound.ParserCombinators (is)
import Bookhound.Utils.Array (hasSome)
import Control.Monad.Rec.Class (Step(..), tailRecM)
import Control.Monad.State (State)
import Data.Array as Array
import Data.Array.NonEmpty as NonEmptyArray
import Data.HashMap as HashMap
@@ -30,12 +33,13 @@ type LocalFormulaCtx =
, localFnsMap :: HashMap (Scope /\ Var) FnInfo
, argsMap :: HashMap (Scope /\ Var) FnInfo
, module' :: Module
, fnInfo :: Maybe FnInfo
, scope :: Scope
, scopeLoc :: Loc Scope
, lambdaCount :: Int
}

type EvalM a = forall m. MonadState LocalFormulaCtx m => ExceptT EvalError m a
type EvalM a = ExceptT EvalError (State LocalFormulaCtx) a

registerBindings :: Array FnDef -> EvalM Unit
registerBindings bindings = do
@@ -49,6 +53,18 @@ registerBindings bindings = do
st.scopeLoc
}

tailCallWrapper :: EvalM Object -> EvalM Object
tailCallWrapper action = untilLoopEnd \argObjs -> do
when (hasSome argObjs) do
{ fnInfo } <- get
(put =<< getNewFnState (unsafeFromJust fnInfo) (Object' <$> argObjs))
action

untilLoopEnd :: (Array Object -> EvalM Object) -> EvalM Object
untilLoopEnd action = [] # tailRecM \args -> action args <#> case _ of
LoopObj x -> Loop x
x -> Done x

registerLocalFn :: Scope -> FnDef -> EvalM Unit
registerLocalFn scope fnDef =
modify_ \st -> st { localFnsMap = insertFnDef scope fnDef st.localFnsMap }
@@ -150,23 +166,28 @@ insertFnDef scope (FnDef fnName params returnType doc body) =
}

getNewFnState :: FnInfo -> Array FnBody -> EvalM LocalFormulaCtx
getNewFnState (FnInfo { id: maybeFnId, scope, params, argsMap }) fnArgs =
getNewFnState
fnInfo@(FnInfo { id: maybeFnId, scope, params, argsMap })
fnArgs =
do
st <- get
let
newArgsMap = HashMap.union argsMap $ HashMap.union argBindings st.argsMap
newArgsMap = HashMap.union argsMap
$ HashMap.union argBindings st.argsMap
pure $ case maybeFnId of
Just { fnModule } ->
st
{ argsMap = newArgsMap
, localFnsMap = HashMap.empty
, module' = fnModule
, fnInfo = Just fnInfo
, scope = zero
, scopeLoc = fromTree $ mkLeaf zero
}
Nothing -> st
{ argsMap = newArgsMap
, scope = scope
, fnInfo = Just fnInfo
, scopeLoc = goToNode scope st.scopeLoc
}
where
36 changes: 20 additions & 16 deletions src/Evaluator/Expression.purs
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ module App.Evaluator.Expression where

import FatPrelude

import App.Evaluator.Common (EvalM, LocalFormulaCtx, extractAlias, getNewFnState, isSpread, lambdaId, lookupBuiltinFn, lookupFn, lookupOperator, registerArg, registerBindings, resetFnScope, substituteFnArgs, varFn)
import App.Evaluator.Common (EvalM, LocalFormulaCtx, extractAlias, getNewFnState, isSpread, lambdaId, lookupBuiltinFn, lookupFn, lookupOperator, registerArg, registerBindings, resetFnScope, substituteFnArgs, tailCallWrapper, varFn)
import App.Evaluator.Errors (EvalError(..), MatchError(..), TypeError(..), raiseError)
import App.Evaluator.Object (cellValueToObj, extractBool, extractNList)
import App.SyntaxTree.Common (QVar(..), Var(..))
@@ -23,6 +23,10 @@ evalExpr (FnApply fnExpr args) = do
BuiltinFnObj fnInfo -> evalBuiltinFn fnInfo argObjs
_ -> raiseError $ TypeError' $ NotAFunction fnObj

evalExpr (Recur args) = do
argObjs <- traverse (\x -> evalExpr x) args
pure $ LoopObj argObjs

evalExpr (LambdaFn params body) = do
{ lambdaCount } <- modify \st -> st { lambdaCount = inc st.lambdaCount }
let lambdaVar = Var $ lambdaId lambdaCount
@@ -49,20 +53,10 @@ evalExpr (RightOpSection body fnOp) = do
evalExpr (WhereExpr fnBody bindings) =
registerBindings bindings *> evalExpr fnBody

evalExpr (CondExpr conds) = do
st <- get
let
newScope = inc $ fromMaybe st.scope $ maximum $ toTree st.scopeLoc
newSt = st
{ scopeLoc = insertChild (mkLeaf newScope) st.scopeLoc
, scope = newScope
}
except $
findMapEither (MatchError' NonExhaustiveGuard)
(evalGuardedFnBody newSt)
conds
evalExpr (CondExpr conds) =
tailCallWrapper $ evalMaybeGuardedFnBody $ Guarded conds

evalExpr (SwitchExpr matchee cases) = do
evalExpr (SwitchExpr matchee cases) = tailCallWrapper do
result <- evalExpr matchee
st <- get
let
@@ -237,8 +231,18 @@ evalCaseBinding st matchee (CaseBinding pattern body) =
evalMaybeGuardedFnBody
:: MaybeGuardedFnBody
-> EvalM Object
evalMaybeGuardedFnBody (Guarded conds) =
evalExpr $ CondExpr conds
evalMaybeGuardedFnBody (Guarded conds) = do
st <- get
let
newScope = inc $ fromMaybe st.scope $ maximum $ toTree st.scopeLoc
newSt = st
{ scopeLoc = insertChild (mkLeaf newScope) st.scopeLoc
, scope = newScope
}
except $
findMapEither (MatchError' NonExhaustiveGuard)
(evalGuardedFnBody newSt)
conds

evalMaybeGuardedFnBody (Standard body) =
evalExpr body
3 changes: 3 additions & 0 deletions src/Interpreter/Formula.purs
Original file line number Diff line number Diff line change
@@ -45,6 +45,9 @@ extractCells :: FnBody -> Set Cell
extractCells (FnApply fnExpr args) =
extractCells fnExpr <> foldMap extractCells args

extractCells (Recur args) =
foldMap extractCells args

extractCells (LambdaFn _ body) =
extractCells body

4 changes: 4 additions & 0 deletions src/Parser/FnDef.purs
Original file line number Diff line number Diff line change
@@ -48,6 +48,9 @@ fnBody = whereExpr <|> topLevelExpr
<$> token fnForm
<*> argListOf openForm

recur = defer \_ -> Recur
<$> (isToken "recur" *> argListOf openForm)

lambdaFn = defer \_ -> LambdaFn
<$> (argListOf var <|> pure <$> var)
<*> (isToken "->" *> fnBody)
@@ -99,6 +102,7 @@ fnBody = whereExpr <|> topLevelExpr
<|> arrayRange
<|> (CellValue' <$> cellValue)
<|> (Cell' <$> cell)
<|> recur
<|> fnOp
<|> fnApply
<|> fnVar
1 change: 0 additions & 1 deletion src/Routes.purs
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@ import FatPrelude hiding (optional, sum, (/))
import App.Editor.Suggestion (SuggestionTerm(..), showFullTerm)
import App.Parser.Common (module', qVar, qVarOp)
import Bookhound.Parser (runParser)
import Data.Generic.Rep (class Generic)
import Halogen.Hooks (Hook)
import Halogen.Hooks.HookM (HookM)
import Halogen.Router.Class (class MonadRouter)
Loading

0 comments on commit b29d326

Please sign in to comment.