packages feed

tramaj-hs 0.4.0.0 → 0.5.0.0

raw patch · 8 files changed

+543/−212 lines, 8 filesdep ~megaparsecPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: megaparsec

API changes (from Hackage documentation)

- Tramaj.Ast: instance GHC.Show.Show Tramaj.Ast.ActionAdaptation
- Tramaj.Ast: instance GHC.Show.Show Tramaj.Ast.Attribute
- Tramaj.Ast: instance GHC.Show.Show Tramaj.Ast.Expr
- Tramaj.Ast: instance GHC.Show.Show Tramaj.Ast.ParamValue
- Tramaj.Ast: instance GHC.Show.Show Tramaj.Ast.Program
- Tramaj.Ast: instance GHC.Show.Show Tramaj.Ast.Stmt
- Tramaj.Ast: instance GHC.Show.Show Tramaj.Ast.TypeConstraintArg
- Tramaj.Ast: instance GHC.Show.Show Tramaj.Ast.TypeExpr
- Tramaj.Eval: instance GHC.Base.Applicative Tramaj.Eval.Eval
- Tramaj.Eval: instance GHC.Base.Functor Tramaj.Eval.Eval
- Tramaj.Eval: instance GHC.Base.Monad Tramaj.Eval.Eval
- Tramaj.Eval: instance GHC.Base.Monoid Tramaj.Eval.Emissions
- Tramaj.Eval: instance GHC.Base.Semigroup Tramaj.Eval.Emissions
- Tramaj.Eval: instance GHC.Show.Show Tramaj.Eval.EvalError
- Tramaj.Eval: instance GHC.Show.Show Tramaj.Eval.Mode
- Tramaj.Eval: instance GHC.Show.Show Tramaj.Eval.Options
- Tramaj.Eval: instance GHC.Show.Show Tramaj.Eval.Output
- Tramaj.Eval: instance GHC.Show.Show Tramaj.Eval.SymbolEntry
- Tramaj.Eval: instance GHC.Show.Show Tramaj.Eval.SymbolOrigin
- Tramaj.Json: instance GHC.Show.Show Tramaj.Json.Json
- Tramaj.Node: instance GHC.Show.Show Tramaj.Node.Node
- Tramaj.Node: instance GHC.Show.Show Tramaj.Node.NodeAttribute
- Tramaj.Types: instance GHC.Show.Show Tramaj.Types.ResolvedConstraintArg
- Tramaj.Types: instance GHC.Show.Show Tramaj.Types.ResolvedType
- Tramaj.Types: instance GHC.Show.Show Tramaj.Types.TypeError
+ Tramaj.Ast: instance GHC.Internal.Show.Show Tramaj.Ast.ActionAdaptation
+ Tramaj.Ast: instance GHC.Internal.Show.Show Tramaj.Ast.Attribute
+ Tramaj.Ast: instance GHC.Internal.Show.Show Tramaj.Ast.Expr
+ Tramaj.Ast: instance GHC.Internal.Show.Show Tramaj.Ast.ParamValue
+ Tramaj.Ast: instance GHC.Internal.Show.Show Tramaj.Ast.Program
+ Tramaj.Ast: instance GHC.Internal.Show.Show Tramaj.Ast.Stmt
+ Tramaj.Ast: instance GHC.Internal.Show.Show Tramaj.Ast.TypeConstraintArg
+ Tramaj.Ast: instance GHC.Internal.Show.Show Tramaj.Ast.TypeExpr
+ Tramaj.Ast: markerSite :: Int
+ Tramaj.Ast: shapeKeyName :: Text
+ Tramaj.Eval: instance GHC.Internal.Base.Applicative Tramaj.Eval.Eval
+ Tramaj.Eval: instance GHC.Internal.Base.Functor Tramaj.Eval.Eval
+ Tramaj.Eval: instance GHC.Internal.Base.Monad Tramaj.Eval.Eval
+ Tramaj.Eval: instance GHC.Internal.Base.Monoid Tramaj.Eval.Emissions
+ Tramaj.Eval: instance GHC.Internal.Base.Semigroup Tramaj.Eval.Emissions
+ Tramaj.Eval: instance GHC.Internal.Show.Show Tramaj.Eval.EvalError
+ Tramaj.Eval: instance GHC.Internal.Show.Show Tramaj.Eval.Mode
+ Tramaj.Eval: instance GHC.Internal.Show.Show Tramaj.Eval.Options
+ Tramaj.Eval: instance GHC.Internal.Show.Show Tramaj.Eval.Output
+ Tramaj.Eval: instance GHC.Internal.Show.Show Tramaj.Eval.SymbolEntry
+ Tramaj.Eval: instance GHC.Internal.Show.Show Tramaj.Eval.SymbolOrigin
+ Tramaj.Json: instance GHC.Internal.Show.Show Tramaj.Json.Json
+ Tramaj.Node: instance GHC.Internal.Show.Show Tramaj.Node.Node
+ Tramaj.Node: instance GHC.Internal.Show.Show Tramaj.Node.NodeAttribute
+ Tramaj.Types: instance GHC.Internal.Show.Show Tramaj.Types.ResolvedConstraintArg
+ Tramaj.Types: instance GHC.Internal.Show.Show Tramaj.Types.ResolvedType
+ Tramaj.Types: instance GHC.Internal.Show.Show Tramaj.Types.TypeError

Files

CHANGELOG.md view
@@ -1,5 +1,34 @@ # Changelog +## 0.5.0.0 (2026-10-24)++**`?shape(key, shape)` allocates a structure of symbols** with one key and+one site (`../specs/v3-symbols.md` \S1.2, `../specs/decisions.md` \S19). Each+`~name` or `~name(e)` marker in the shape is a symbol `[key, "name"]` or+`[key, "name", e]` at the form's site, and `{~name}` is short for+`{name: ~name}`. It lowers at parse time, so the AST and the envelope are+unchanged. A marker outside a shape or in a key, a bare marker under a lambda+in the shape, a marker name used twice, a shape with no marker and a wrong+argument count are parse errors. New syntax, hence the minor bump; a program+that parsed before parses to the same AST. The corpus runner provides the+`shape` profile. New in `Tramaj.Ast`: `shapeKeyName` and `markerSite`.++**Breaking, symbolic mode only: a symbol or a term as the key of `has` or+`lookup` is `NotConcrete`** (`../specs/v3-symbols.md` \S1.5), where it+answered `false` or the fallback, reading an unknown key as a missing one. A+concrete key keeps the tolerant behaviour: a missing key, an out-of-range+index or a wrong-shaped container still answers `false` or the fallback.++**Parse errors point at the mistake.** An unclosed `(`, `[`, `{`, string or+interpolation used to be reported at line 1, column 1 (megaparsec's error+from wherever the outermost backtracking alternative started), whatever the+mistake. A `try` now covers only what tells a form from its alternatives,+never the form's body, so the error is reported where the closer was looked+for, and the closer names its opener and where it is: `expecting ')' to+close the '(' of .div at line 1, column 5`. Every program that parsed still+parses to the same AST; only the position and wording of errors change.+tramaj-purs has the same fix.+ ## 0.4.0.0  **Breaking: integers and floats are two number types** (`../specs/decisions.md`
src/Tramaj/Ast.hs view
@@ -41,9 +41,12 @@   , subExprs   , attributeExprs   , numberAllocs+  , shapeKeyName+  , markerSite   ) where  import Data.Int (Int64)+import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T @@ -152,7 +155,9 @@     -- being derived some other way. Assigned by 'numberAllocs' after     -- parsing, not by the parser itself, so that agreement between     -- implementations rests on one small pure function over the AST rather-    -- than on two parsers' internal traversal order.+    -- than on two parsers' internal traversal order. A @~name@ marker of a+    -- @?shape@ (decisions \S19) is one too, at its form's site+    -- ('shapeKeyName').     Alloc Int Expr   | -- | @?ctx.a.b@ (\S1.3): declares a context read symbolic. The path MUST     -- be rooted at @ctx@, which is why it is not just a 'Path' -- unlike an@@ -390,9 +395,27 @@ typeDecls :: [Stmt] -> [(Text, TypeExpr)] typeDecls stmts = [(n, t) | STypeDecl n t <- stmts] +-- | The hidden name a @?shape(key, shape)@ (v3-symbols \S1.2, decisions+-- \S19) binds its key to. The form lowers to @Let shapeKeyName key shape@,+-- with each marker in the shape an 'Alloc' whose key reads this name, so the+-- core AST needs no constructor of its own for it. It starts with @#@, so no+-- surface name can collide with it ('isHiddenName'), and only the parser+-- writes such a 'Let': 'numberAllocs' takes one as a @?shape@ site.+shapeKeyName :: Text+shapeKeyName = "#shape"++-- | The site a @~name@ marker carries between the parser and+-- 'numberAllocs', which replaces it with the site of the innermost enclosing+-- @?shape@. Never left in a parsed program: the parser refuses a marker+-- outside a shape.+markerSite :: Int+markerSite = -1+ -- | Assigns each 'Alloc' in a program the index of its @?(...)@ among all of -- them, in source order (v3-symbols \S1.4) -- a plain pre-order, left-to-right--- walk over the freshly parsed tree, numbering as it goes.+-- walk over the freshly parsed tree, numbering as it goes. A @?shape@ (its+-- 'shapeKeyName' 'Let') takes a number in the same sequence, at its @?@,+-- before its key; its markers take none, only its site. -- -- Done as a rewrite after parsing, rather than by a counter threaded through -- the parser itself, so that two independently written parsers agree on site@@ -405,122 +428,136 @@ -- produces -- attributes before value before children in an 'Element', -- condition before either arm in a 'Branch', and so on. numberAllocs :: Expr -> Expr-numberAllocs e = snd (go 0 e)+numberAllocs e = snd (numberIn Nothing 0 e)   where-    go :: Int -> Expr -> (Int, Expr)-    go n (Alloc _ key) =-      let (n', key') = go (n + 1) key-       in (n', Alloc n key')-    go n e'@(Path _ _) = (n, e')-    go n (FieldAccess target fields) =-      let (n', target') = go n target in (n', FieldAccess target' fields)-    go n (Call fn args) =-      let (n1, fn') = go n fn-          (n2, args') = goList n1 args-       in (n2, Call fn' args')-    go n (Lambda params body) = let (n', body') = go n body in (n', Lambda params body')-    go n (Let name value body) =-      let (n1, value') = go n value-          (n2, body') = go n1 body-       in (n2, Let name value' body')-    go n e'@(StringLit _) = (n, e')-    go n e'@(IntLit _) = (n, e')-    go n e'@(FloatLit _) = (n, e')-    go n e'@(BoolLit _) = (n, e')-    go n NullLit = (n, NullLit)-    go n (ArrayLit elems) = let (n', elems') = goList n elems in (n', ArrayLit elems')-    go n (ObjectLit entries) =-      let (n', entries') = goPairs n entries in (n', ObjectLit entries')-    go n (Element tag attrs val children) =-      let (n1, attrs') = goAttrs n attrs-          (n2, val') = go n1 val-          (n3, children') = goList n2 children-       in (n3, Element tag attrs' val' children')-    go n (Fragment children) = let (n', children') = goList n children in (n', Fragment children')-    go n (Branch c t e') =-      let (n1, c') = go n c-          (n2, t') = go n1 t-          (n3, e'') = go n2 e'-       in (n3, Branch c' t' e'')-    go n (Map coll fn) =-      let (n1, coll') = go n coll-          (n2, fn') = go n1 fn-       in (n2, Map coll' fn')-    go n (Filter coll fn) =-      let (n1, coll') = go n coll-          (n2, fn') = go n1 fn-       in (n2, Filter coll' fn')-    go n (Scan coll initial fn) =-      let (n1, coll') = go n coll-          (n2, initial') = go n1 initial-          (n3, fn') = go n2 fn-       in (n3, Scan coll' initial' fn')-    go n (Fold coll initial fn) =-      let (n1, coll') = go n coll-          (n2, initial') = go n1 initial-          (n3, fn') = go n2 fn-       in (n3, Fold coll' initial' fn')-    go n (SortBy descending coll fn) =-      let (n1, coll') = go n coll-          (n2, fn') = go n1 fn-       in (n2, SortBy descending coll' fn')-    go n (Concat l r) =-      let (n1, l') = go n l-          (n2, r') = go n1 r-       in (n2, Concat l' r')-    go n (Import name params) =-      let (n', params') = goParams n params in (n', Import name params')-    go n (AdaptActions target adaptation fn) =-      let (n1, target') = go n target-          (n2, fn') = case fn of-            Nothing -> (n1, Nothing)-            Just f -> let (n', f') = go n1 f in (n', Just f')-       in (n2, AdaptActions target' adaptation fn')-    go n (Constrain name args) = let (n', args') = goList n args in (n', Constrain name args')-    go n (Emit constraint body) =-      let (n1, constraint') = go n constraint-          (n2, body') = go n1 body-       in (n2, Emit constraint' body')-    go n e'@(Demand _) = (n, e')-    go n (TypeDecl name t body) = let (n', body') = go n body in (n', TypeDecl name t body')-    go n (TypeAnnotate name t value body) =-      let (n1, value') = go n value-          (n2, body') = go n1 body-       in (n2, TypeAnnotate name t value' body')-    go n (TypeEmit name tcArgs body) = let (n', body') = go n body in (n', TypeEmit name tcArgs body')+    -- | Numbers under the innermost enclosing @?shape@'s site, if any: a+    -- marker ('markerSite') takes that site, and no number of its own.+    numberIn :: Maybe Int -> Int -> Expr -> (Int, Expr)+    numberIn shape = go+      where+        go :: Int -> Expr -> (Int, Expr)+        go n (Alloc site key)+          | site == markerSite =+              let (n', key') = go n key+               in (n', Alloc (fromMaybe markerSite shape) key')+          | otherwise =+              let (n', key') = go (n + 1) key+               in (n', Alloc n key')+        go n e'@(Path _ _) = (n, e')+        go n (FieldAccess target fields) =+          let (n', target') = go n target in (n', FieldAccess target' fields)+        go n (Call fn args) =+          let (n1, fn') = go n fn+              (n2, args') = goList n1 args+           in (n2, Call fn' args')+        go n (Lambda params body) = let (n', body') = go n body in (n', Lambda params body')+        go n (Let name value body)+          | name == shapeKeyName =+              let (n1, value') = go (n + 1) value+                  (n2, body') = numberIn (Just n) n1 body+               in (n2, Let name value' body')+        go n (Let name value body) =+          let (n1, value') = go n value+              (n2, body') = go n1 body+           in (n2, Let name value' body')+        go n e'@(StringLit _) = (n, e')+        go n e'@(IntLit _) = (n, e')+        go n e'@(FloatLit _) = (n, e')+        go n e'@(BoolLit _) = (n, e')+        go n NullLit = (n, NullLit)+        go n (ArrayLit elems) = let (n', elems') = goList n elems in (n', ArrayLit elems')+        go n (ObjectLit entries) =+          let (n', entries') = goPairs n entries in (n', ObjectLit entries')+        go n (Element tag attrs val children) =+          let (n1, attrs') = goAttrs n attrs+              (n2, val') = go n1 val+              (n3, children') = goList n2 children+           in (n3, Element tag attrs' val' children')+        go n (Fragment children) = let (n', children') = goList n children in (n', Fragment children')+        go n (Branch c t e') =+          let (n1, c') = go n c+              (n2, t') = go n1 t+              (n3, e'') = go n2 e'+           in (n3, Branch c' t' e'')+        go n (Map coll fn) =+          let (n1, coll') = go n coll+              (n2, fn') = go n1 fn+           in (n2, Map coll' fn')+        go n (Filter coll fn) =+          let (n1, coll') = go n coll+              (n2, fn') = go n1 fn+           in (n2, Filter coll' fn')+        go n (Scan coll initial fn) =+          let (n1, coll') = go n coll+              (n2, initial') = go n1 initial+              (n3, fn') = go n2 fn+           in (n3, Scan coll' initial' fn')+        go n (Fold coll initial fn) =+          let (n1, coll') = go n coll+              (n2, initial') = go n1 initial+              (n3, fn') = go n2 fn+           in (n3, Fold coll' initial' fn')+        go n (SortBy descending coll fn) =+          let (n1, coll') = go n coll+              (n2, fn') = go n1 fn+           in (n2, SortBy descending coll' fn')+        go n (Concat l r) =+          let (n1, l') = go n l+              (n2, r') = go n1 r+           in (n2, Concat l' r')+        go n (Import name params) =+          let (n', params') = goParams n params in (n', Import name params')+        go n (AdaptActions target adaptation fn) =+          let (n1, target') = go n target+              (n2, fn') = case fn of+                Nothing -> (n1, Nothing)+                Just f -> let (n', f') = go n1 f in (n', Just f')+           in (n2, AdaptActions target' adaptation fn')+        go n (Constrain name args) = let (n', args') = goList n args in (n', Constrain name args')+        go n (Emit constraint body) =+          let (n1, constraint') = go n constraint+              (n2, body') = go n1 body+           in (n2, Emit constraint' body')+        go n e'@(Demand _) = (n, e')+        go n (TypeDecl name t body) = let (n', body') = go n body in (n', TypeDecl name t body')+        go n (TypeAnnotate name t value body) =+          let (n1, value') = go n value+              (n2, body') = go n1 body+           in (n2, TypeAnnotate name t value' body')+        go n (TypeEmit name tcArgs body) = let (n', body') = go n body in (n', TypeEmit name tcArgs body') -    goList :: Int -> [Expr] -> (Int, [Expr])-    goList n [] = (n, [])-    goList n (x : xs) =-      let (n1, x') = go n x-          (n2, xs') = goList n1 xs-       in (n2, x' : xs')+        goList :: Int -> [Expr] -> (Int, [Expr])+        goList n [] = (n, [])+        goList n (x : xs) =+          let (n1, x') = go n x+              (n2, xs') = goList n1 xs+           in (n2, x' : xs') -    goPairs :: Int -> [(Text, Expr)] -> (Int, [(Text, Expr)])-    goPairs n [] = (n, [])-    goPairs n ((k, x) : xs) =-      let (n1, x') = go n x-          (n2, xs') = goPairs n1 xs-       in (n2, (k, x') : xs')+        goPairs :: Int -> [(Text, Expr)] -> (Int, [(Text, Expr)])+        goPairs n [] = (n, [])+        goPairs n ((k, x) : xs) =+          let (n1, x') = go n x+              (n2, xs') = goPairs n1 xs+           in (n2, (k, x') : xs') -    goAttrs :: Int -> [Attribute] -> (Int, [Attribute])-    goAttrs n [] = (n, [])-    goAttrs n (Attr name x : xs) =-      let (n1, x') = go n x-          (n2, xs') = goAttrs n1 xs-       in (n2, Attr name x' : xs')-    goAttrs n (ActionAttr ev key x : xs) =-      let (n1, x') = go n x-          (n2, xs') = goAttrs n1 xs-       in (n2, ActionAttr ev key x' : xs')+        goAttrs :: Int -> [Attribute] -> (Int, [Attribute])+        goAttrs n [] = (n, [])+        goAttrs n (Attr name x : xs) =+          let (n1, x') = go n x+              (n2, xs') = goAttrs n1 xs+           in (n2, Attr name x' : xs')+        goAttrs n (ActionAttr ev key x : xs) =+          let (n1, x') = go n x+              (n2, xs') = goAttrs n1 xs+           in (n2, ActionAttr ev key x' : xs') -    goParams :: Int -> [(Text, ParamValue)] -> (Int, [(Text, ParamValue)])-    goParams n [] = (n, [])-    goParams n ((k, PExpr x) : xs) =-      let (n1, x') = go n x-          (n2, xs') = goParams n1 xs-       in (n2, (k, PExpr x') : xs')-    goParams n ((k, p@(PFromContext _)) : xs) =-      let (n1, xs') = goParams n xs in (n1, (k, p) : xs')-    goParams n ((k, p@(PType _)) : xs) =-      let (n1, xs') = goParams n xs in (n1, (k, p) : xs')+        goParams :: Int -> [(Text, ParamValue)] -> (Int, [(Text, ParamValue)])+        goParams n [] = (n, [])+        goParams n ((k, PExpr x) : xs) =+          let (n1, x') = go n x+              (n2, xs') = goParams n1 xs+           in (n2, (k, PExpr x') : xs')+        goParams n ((k, p@(PFromContext _)) : xs) =+          let (n1, xs') = goParams n xs in (n1, (k, p) : xs')+        goParams n ((k, p@(PType _)) : xs) =+          let (n1, xs') = goParams n xs in (n1, (k, p) : xs')
src/Tramaj/Eval.hs view
@@ -1346,12 +1346,14 @@      -- | Deliberately tolerant: a missing key, an out-of-range index, or a     -- container of the wrong shape all answer @false@ rather than erroring-    -- -- except a symbolic container, which is 'NotConcrete' rather than a-    -- lie (v3-symbols \S1.5): the tolerant @false@ would claim to know-    -- something about a container the language cannot see into.+    -- -- except a symbolic container or key, which is 'NotConcrete' rather+    -- than a lie (v3-symbols \S1.5): the tolerant @false@ would claim to+    -- know something about a value the language cannot see into.     hasImpl :: Value' -> Value' -> Either EvalError Value'     hasImpl (VSymbol _ _) _ = Left (NotConcrete name)     hasImpl (VTerm _ _) _ = Left (NotConcrete name)+    hasImpl _ (VSymbol _ _) = Left (NotConcrete name)+    hasImpl _ (VTerm _ _) = Left (NotConcrete name)     hasImpl container key = Right . VBool $ case (container, key) of       (VObject o, VString k) -> Map.member k o       (VArray xs, VInt n) -> maybe False (\i -> i < length xs) (asIndex n)@@ -1359,11 +1361,13 @@      -- | Dynamic access by a computed key or index -- the counterpart to a     -- static path segment. The third argument is the mandatory fallback. A-    -- symbolic container is 'NotConcrete' rather than falling back, which+    -- symbolic container or key is 'NotConcrete' rather than falling back, which     -- would silently discard the symbol (\S1.5).     lookupImpl :: Value' -> Value' -> Value' -> Either EvalError Value'     lookupImpl (VSymbol _ _) _ _ = Left (NotConcrete name)     lookupImpl (VTerm _ _) _ _ = Left (NotConcrete name)+    lookupImpl _ (VSymbol _ _) _ = Left (NotConcrete name)+    lookupImpl _ (VTerm _ _) _ = Left (NotConcrete name)     lookupImpl container key fallback = Right $ case (container, key) of       (VObject o, VString k) -> maybe fallback id (Map.lookup k o)       (VArray xs, VInt n) -> maybe fallback id (asIndex n >>= atIndex xs)
src/Tramaj/Parser.hs view
@@ -5,6 +5,8 @@ -- * string interpolation becomes 'Concat' over the @str@ builtin; -- * @branch(fallback, p1, v1, ...)@ becomes nested 'Branch'; -- * object shorthand @{foo}@ becomes @{"foo": $foo}@;+-- * @?shape(key, shape)@ becomes a 'Let' of its key and the shape, each+--   @~name@ marker in it an 'Alloc' at the form's one site; -- * escape sequences are resolved into the 'StringLit' they denote. -- -- The grammar keeps three leader characters from v1: @.@ introduces a@@ -24,6 +26,13 @@ --   @map@ has matched, its shape is parsed without backtracking, so a --   malformed one is a hard parse error instead of silently falling through --   to a meaningless 'Call' that would only fail much later at eval time.+--   The same holds for every form, not only the special ones: a 'try'+--   covers what tells a form from its alternatives (@name(@, @key:@,+--   @(params) =>@) and never the form's body. A 'try' around a body throws+--   away the error inside it, and the program is then reported as wrong at+--   wherever the outermost alternative started -- line 1, column 1 for an+--   unclosed bracket anywhere in a document. Each closer also names its+--   opener and where it is ('closing'). -- * A field-access suffix is parsed with no whitespace skipped before it, so --   @f().rendered@ is a field access while @f()@ followed by a newline and --   @.div(...)@ is two separate things.@@ -35,7 +44,7 @@ import Data.Char (chr, isAlphaNum, isDigit, isHexDigit, isLetter) import Data.Int (Int64) import Data.List (nub)-import Data.Maybe (fromMaybe)+import Data.Maybe (fromMaybe, maybeToList) import Data.Text (Text) import qualified Data.Text as T import Data.Void (Void)@@ -82,6 +91,50 @@ lexeme :: P a -> P a lexeme p = p <* skipSpaces +-- Unclosed openers ---------------------------------------------------------++-- | An opener that has been read -- a bracket, a quote, a backtick -- and+-- where it sits, so the closer that never comes can say which one it was.+--+-- The error for a missing closer is reported where the closer was looked+-- for (often the end of the input), and names the opener and its position:+-- @expecting ')' to close the '(' at line 1, column 5@. Without that, the+-- only position a reader got was wherever the outermost backtracking+-- alternative started, which for most programs is line 1, column 1.+data Opened = Opened String SourcePos++-- | Reads an opener with @p@, remembering where it started. @what@ is how+-- the closer's error names it: @'('@, @the string@, @the interpolation@.+opening :: String -> P a -> P Opened+opening what p = do+  pos <- getSourcePos+  _ <- p+  pure (Opened what pos)++-- | Reads the closer that matches an 'Opened', labelled with it.+closing :: Opened -> String -> P a -> P a+closing (Opened what pos) closer p =+  p <?> (closer <> " to close " <> what <> " at line " <> show (unPos (sourceLine pos)) <> ", column " <> show (unPos (sourceColumn pos)))++-- | The common case: a lexeme opener and its lexeme closer.+openSym :: Text -> P Opened+openSym o = opening ("the '" <> T.unpack o <> "'") (symbol o)++closeSym :: Opened -> Text -> P ()+closeSym opened c = () <$ closing opened ("'" <> T.unpack c <> "'") (symbol c)++-- | A closer read with no whitespace skipped after it, for the forms that+-- read a field-access suffix directly behind their @)@.+closeChar :: Opened -> Char -> P ()+closeChar opened c = () <$ closing opened ("'" <> [c] <> "'") (char c)++-- | Another item of a comma-separated list: a @,@ that is not the trailing+-- one before the closer @c@. Recognizing the comma this way, rather than+-- wrapping the comma /and the item/ in a 'try', is what keeps an error inside+-- the item where it happened instead of rewinding to the comma.+nextItem :: Char -> P ()+nextItem c = () <$ try (symbol "," <* notFollowedBy (char c))+ symbol :: Text -> P Text symbol s = lexeme (string s) @@ -135,9 +188,12 @@ -- whole point of the position being static is that it cannot be computed. staticString :: P Text staticString = lexeme $ do-  _ <- char '"'+  opened <- opening "the string" (char '"')   s <- takeWhileP Nothing (\c -> c /= '"' && c /= '`')-  _ <- char '"' <|> interpolationRefused+  next <- optional (lookAhead anySingle)+  case next of+    Just '`' -> interpolationRefused+    _ -> () <$ closing opened "'\"'" (char '"')   pure s   where     interpolationRefused =@@ -149,16 +205,20 @@ -- arbitrary expression. stringLit :: P Expr stringLit = lexeme $ do-  _ <- char '"'+  opened <- opening "the string" (char '"')   parts <- many stringPart-  _ <- char '"'+  _ <- closing opened "'\"'" (char '"')   pure (desugarString parts)   where     stringPart :: P StringPart     stringPart = interpPart <|> (SLit <$> litChunk)      interpPart :: P StringPart-    interpPart = SInterp <$> (char '`' *> expr <* char '`')+    interpPart = do+      opened <- opening "the interpolation" (char '`')+      e <- expr+      _ <- closing opened "'`'" (char '`')+      pure (SInterp e)      -- | A run of ordinary characters, with escape sequences resolved as they     -- are read. Stops at a closing quote or an interpolation's backtick;@@ -190,9 +250,9 @@     unicodeEscape :: P Text     unicodeEscape = do       _ <- char 'u'-      _ <- char '{'+      opened <- opening "the '{' of the unicode escape" (char '{')       digits <- takeWhile1P (Just "hex digit") isHexDigit-      _ <- char '}'+      _ <- closing opened "'}'" (char '}')       case readHex (T.unpack digits) of         [(n, "")] | n <= 0x10FFFF -> pure (T.singleton (chr n))         _ -> fail ("invalid unicode escape: \\u{" <> T.unpack digits <> "}")@@ -277,26 +337,34 @@  arrayLit :: P Expr arrayLit = lexeme $ do-  _ <- symbol "["+  opened <- openSym "["   elems <- sepEndBy expr (symbol ",")-  _ <- symbol "]"+  closeSym opened "]"   pure (ArrayLit elems)  -- | Object keys may be quoted or bare, and a bare key on its own is shorthand -- for reading the binding of the same name: @{foo, bar: $baz}@. objectLit :: P Expr objectLit = lexeme $ do-  _ <- symbol "{"+  opened <- openSym "{"   entries <- sepEndBy objEntry (symbol ",")-  _ <- symbol "}"+  closeSym opened "}"   pure (ObjectLit entries)   where     objEntry :: P (Text, Expr)-    objEntry = try explicitEntry <|> shorthandEntry+    objEntry = explicitEntry <|> markerEntry <|> shorthandEntry +    -- | @{~replicas}@ is @{replicas: ~replicas}@, and @{~host($x)}@ is+    -- @{host: ~host($x)}@ (v3-symbols \S1.2): the shorthand field of a+    -- marker, expanded here so every other rule sees the long form.+    markerEntry :: P (Text, Expr)+    markerEntry = marker++    -- The 'try' covers @key:@ only, which is what tells this from the+    -- shorthand; an error in the value is reported where it is.     explicitEntry :: P (Text, Expr)     explicitEntry = do-      k <- objectKey+      k <- try (objectKey <* lookAhead (symbol ":"))       _ <- reservedKeyRefused k       _ <- symbol ":"       v <- expr@@ -336,15 +404,97 @@ -- traversal order. A projection following it, as in @?(key).field@, is an -- ordinary field-access suffix, the same mechanism a call result uses. allocExpr :: P Expr-allocExpr = try $ do-  _ <- char '?'-  _ <- symbol "("+allocExpr = do+  opened <- try $ do+    _ <- char '?'+    openSym "("   keyExpr <- expr-  _ <- char ')'+  closeChar opened ')'   segs <- fieldAccessSuffix   skipSpaces   pure (applyFieldAccess (Alloc 0 keyExpr) segs) +-- | @?shape(key, shape)@ (v3-symbols \S1.2, decisions \S19): a structure of+-- symbols written with one key and one site. It lowers here, so the core AST+-- is unchanged: the key is bound once to the hidden 'shapeKeyName', and each+-- marker in the shape is already an 'Alloc' over @[$#shape, "name"]@ or+-- @[$#shape, "name", key]@ ('marker'). 'numberAllocs' then gives the form+-- one site and its markers that same site.+--+-- The 'try' covers @?shape(@ only; past it, the form is exactly two+-- arguments, and a malformed one is a parse error. So are the marker rules+-- the shape must keep ('checkShape') and a marker in the key.+shapeExpr :: P Expr+shapeExpr = do+  opened <- try $ do+    _ <- char '?'+    n <- rawIdent+    if n == "shape" then pure () else fail "not a ?shape form"+    openSym "("+  keyExpr <- expr+  if null (ownMarkers False keyExpr)+    then pure ()+    else fail "a marker cannot be written in a ?shape's key"+  _ <- symbol ","+  shape <- expr+  closeChar opened ')'+  checkShape shape+  segs <- fieldAccessSuffix+  skipSpaces+  pure (applyFieldAccess (Let shapeKeyName keyExpr shape) segs)++-- | The rules on a @?shape@'s own markers, all static: at least one, no name+-- twice (bare or keyed), and a key on every marker that sits under a lambda+-- inside the shape.+checkShape :: Expr -> P ()+checkShape shape+  | null markers = fail "a ?shape's shape must contain at least one marker of its own"+  | any (\(_, keyed, underLambda) -> underLambda && not keyed) markers =+      fail "a marker under a lambda inside a ?shape must carry a key, as in ~name(key)"+  | length (nub names) /= length names = fail "a marker name appears twice in one ?shape"+  | otherwise = pure ()+  where+    markers = ownMarkers False shape+    names = [n | (n, _, _) <- markers]++-- | @~name@ or @~name(key)@: a symbol marker, as the allocation it lowers to,+-- with 'markerSite' until 'numberAllocs' gives it its form's site. The name+-- follows the @~@ directly, and the @(@ of a key follows the name directly.+-- Parsed anywhere an expression is; the enclosing 'shapeExpr' (or, outside+-- any, 'noStrayMarker') decides whether it is legal there.+marker :: P (Text, Expr)+marker = lexeme $ do+  _ <- char '~'+  name <- rawIdent+  key <- optional $ do+    opened <- opening "the '('" (char '(')+    skipSpaces+    e <- expr+    closeChar opened ')'+    pure e+  pure (name, Alloc markerSite (ArrayLit ([Path shapeKeyName [], StringLit name] <> maybeToList key)))++markerExpr :: P Expr+markerExpr = snd <$> marker++-- | The markers that belong to the form whose shape (or key) this is: every+-- marker in it but those inside a nested @?shape@'s shape, as (name, keyed,+-- under a lambda written inside it).+ownMarkers :: Bool -> Expr -> [(Text, Bool, Bool)]+ownMarkers underLambda e = case e of+  Alloc site (ArrayLit (_ : StringLit name : rest))+    | site == markerSite -> (name, not (null rest), underLambda) : concatMap (ownMarkers underLambda) rest+  Let name value _+    | name == shapeKeyName -> ownMarkers underLambda value+  Lambda _ body -> ownMarkers True body+  _ -> concatMap (ownMarkers underLambda) (subExprs e)++-- | A marker belongs to a @?shape@'s shape; anywhere else it is a parse error.+noStrayMarker :: Expr -> P ()+noStrayMarker e+  | null (ownMarkers False e) = pure ()+  | otherwise = fail "a marker ~name may only be written inside a ?shape's shape"+ -- | @?ctx.a.b@ (\S1.3): the path MUST be rooted at @ctx@ -- unlike an -- ordinary read, an unsupplied demand allocates at the root rather than -- failing, so the language needs to tell the two apart before evaluating@@ -362,22 +512,26 @@ -- values (@$lib.vals.fn(1)@) or an import being given more parameters -- (@$deployment({...})@) is callable directly. call :: P Expr-call = try $ do-  _ <- optional (char '$')-  (root, fields) <- pathTail-  _ <- symbol "("+call = do+  ((root, fields), opened) <- try $ do+    _ <- optional (char '$')+    callee <- pathTail+    opened <- openSym "("+    pure (callee, opened)   args <- sepEndBy expr (symbol ",")-  _ <- char ')'+  closeChar opened ')'   segs <- fieldAccessSuffix   skipSpaces   pure (applyFieldAccess (Call (Path root fields) args) segs)  lambdaExpr :: P Expr-lambdaExpr = try $ do-  _ <- symbol "("-  params <- sepEndBy pattern (symbol ",")-  _ <- symbol ")"-  _ <- symbol "=>"+lambdaExpr = do+  params <- try $ do+    _ <- symbol "("+    params <- sepEndBy pattern (symbol ",")+    _ <- symbol ")"+    _ <- symbol "=>"+    pure params   lowerLambda params <$> expr  -- Binding patterns (decisions \S17) -------------------------------------------@@ -394,9 +548,9 @@  objectPattern :: P Pattern objectPattern = do-  _ <- symbol "{"+  opened <- openSym "{"   fields <- sepEndBy1 patternField (symbol ",")-  _ <- symbol "}"+  closeSym opened "}"   let pat = PObject fields       names = patternNames pat   if length (nub names) /= length names@@ -446,11 +600,14 @@  -- | Grouping, for readability where 'Concat' chains get long. Not a semantic -- construct: the parse tree it produces is the same as the inner expression's.+--+-- No 'try': 'lambdaExpr' is tried first, and nothing else starts with @(@,+-- so an error inside the parentheses is reported where it is. parenExpr :: P Expr-parenExpr = try $ do-  _ <- symbol "("+parenExpr = do+  opened <- openSym "("   e <- expr-  _ <- symbol ")"+  closeSym opened ")"   pure e  -- | The forms whose evaluation the language defines itself, rather than@@ -485,22 +642,22 @@   where     binaryShape :: (Expr -> Expr -> Expr) -> P Expr     binaryShape ctor = do-      _ <- symbol "("+      opened <- openSym "("       a <- expr       _ <- symbol ","       b <- expr-      _ <- char ')'+      closeChar opened ')'       pure (ctor a b)      ternaryShape :: (Expr -> Expr -> Expr -> Expr) -> P Expr     ternaryShape ctor = do-      _ <- symbol "("+      opened <- openSym "("       a <- expr       _ <- symbol ","       b <- expr       _ <- symbol ","       c <- expr-      _ <- char ')'+      closeChar opened ')'       pure (ctor a b c)      -- | @branch(fallback, p1, v1, p2, v2, ...)@ reads "if p1 then v1, else if@@ -508,11 +665,11 @@     -- laziness is the core constructor's rather than a rule of its own.     branchShape :: P Expr     branchShape = do-      _ <- symbol "("+      opened <- openSym "("       fallback <- expr-      arms <- many (try (symbol "," *> arm))+      arms <- many (nextItem ')' *> arm)       _ <- optional (symbol ",")-      _ <- char ')'+      closeChar opened ')'       pure (foldr (\(p, v) acc -> Branch p v acc) fallback arms)      arm :: P (Expr, Expr)@@ -528,11 +685,11 @@     -- signature for any name.     constraintShape :: P Expr     constraintShape = do-      _ <- symbol "("+      opened <- openSym "("       name <- staticString-      args <- many (try (symbol "," *> expr))+      args <- many (nextItem ')' *> expr)       _ <- optional (symbol ",")-      _ <- char ')'+      closeChar opened ')'       pure (Constrain name args)      -- | @import("name", {param: expr, other: ctx(path)})@. The name is a@@ -541,27 +698,26 @@     -- only here.     importShape :: P Expr     importShape = do-      _ <- symbol "("+      opened <- openSym "("       name <- staticString       _ <- symbol ","       params <- importParams-      _ <- char ')'+      closeChar opened ')'       pure (Import name params)      importParams :: P [(Text, ParamValue)]     importParams = do-      _ <- symbol "{"+      opened <- openSym "{"       entries <- sepEndBy paramEntry (symbol ",")-      _ <- symbol "}"+      closeSym opened "}"       pure entries      paramEntry :: P (Text, ParamValue)-    paramEntry = try explicitParam <|> shorthandParam+    paramEntry = explicitParam <|> shorthandParam      explicitParam :: P (Text, ParamValue)     explicitParam = do-      k <- objectKey-      _ <- symbol ":"+      k <- try (objectKey <* symbol ":")       v <- paramValue       pure (k, v) @@ -582,10 +738,10 @@         n <- identifier         _ <- lookAhead (char '(')         if n == ("ctx" :: Text) then pure n else fail "not a ctx(...) parameter"-      _ <- symbol "("+      opened <- openSym "("       (root, fields) <- pathTail       skipSpaces-      _ <- char ')'+      closeChar opened ')'       skipSpaces       pure (PFromContext (root : fields)) @@ -593,13 +749,13 @@     -- the event type and payload.     adaptActionsShape :: P Expr     adaptActionsShape = do-      _ <- symbol "("+      opened <- openSym "("       target <- expr       _ <- symbol ","       adaptation <- adaptationShape-      fn <- optional (try (symbol "," *> expr))+      fn <- optional (nextItem ')' *> expr)       _ <- optional (symbol ",")-      _ <- char ')'+      closeChar opened ')'       pure (AdaptActions target adaptation fn)      -- | Two forms only, never an arbitrary rewriting function: this is what@@ -611,9 +767,9 @@       case name of         "identity" -> pure Identity         "prefix" -> do-          _ <- symbol "("+          opened <- openSym "("           p <- staticString-          _ <- char ')'+          closeChar opened ')'           skipSpaces           pure (Prefix p)         _ -> fail "an action adaptation must be identity or prefix(\"...\")"@@ -682,16 +838,16 @@  typeArray :: P TypeExpr typeArray = do-  _ <- symbol "["+  opened <- openSym "["   t <- typeExpr-  _ <- symbol "]"+  closeSym opened "]"   pure (TArray t)  typeRecord :: P TypeExpr typeRecord = do-  _ <- symbol "{"+  opened <- openSym "{"   fields <- sepEndBy typeField (symbol ",")-  _ <- symbol "}"+  closeSym opened "}"   pure (TRecord fields)   where     -- | Unlike an ordinary object literal's 'objectKey', a field name here is@@ -772,15 +928,16 @@ -- @type-constraint(...)@ would otherwise parse as an ordinary call to an -- unbound name. typeEmission :: P Stmt-typeEmission = try $ do-  _ <- char '!'-  kw <- identifier-  if kw /= ("type-constraint" :: Text) then fail "not a !type-constraint" else pure ()-  _ <- symbol "("+typeEmission = do+  opened <- try $ do+    _ <- char '!'+    kw <- identifier+    if kw /= ("type-constraint" :: Text) then fail "not a !type-constraint" else pure ()+    openSym "("   name <- staticString-  args <- many (try (symbol "," *> typeConstraintArg))+  args <- many (nextItem ')' *> typeConstraintArg)   _ <- optional (symbol ",")-  _ <- char ')'+  closeChar opened ')'   skipSpaces   pure (STypeEmit name args) @@ -790,36 +947,41 @@ -- it -- the same device 'keywordLit' uses -- which is what keeps @typeface = -- ...@ from being chopped into the keyword @type@ plus leftovers. typeDeclStmt :: P Stmt-typeDeclStmt = try $ do-  kw <- identifier-  if kw /= "type" then fail "not a type declaration" else pure ()-  name <- identifier-  _ <- symbol "="+typeDeclStmt = do+  name <- try $ do+    kw <- identifier+    if kw /= "type" then fail "not a type declaration" else pure ()+    name <- identifier+    _ <- symbol "="+    pure name   STypeDecl name <$> typeExpr  -- Documents ---------------------------------------------------------------  -- | @.tag(...)@ is an element; @.(...)@ is a fragment -- a tagless element, -- introducing siblings with no wrapper.+--+-- No 'try': nothing else an operand can be starts with @.@, so once it is+-- read this is a document, and an error inside it is reported where it is. documentExpr :: P Expr-documentExpr = try $ do+documentExpr = do   _ <- char '.'   choice [fragmentShape, elementShape]   where     fragmentShape :: P Expr     fragmentShape = do-      _ <- symbol "("+      opened <- openSym "("       children <- sepEndBy expr (symbol ",")-      _ <- symbol ")"+      closeSym opened ")"       pure (Fragment children)      elementShape :: P Expr     elementShape = do       tag <- rawIdent       skipSpaces-      _ <- symbol "("+      opened <- opening ("the '(' of ." <> T.unpack tag) (symbol "(")       args <- sepEndBy elementArg (symbol ",")-      _ <- symbol ")"+      closeSym opened ")"       buildElement tag args  -- | Buckets an element's arguments and enforces the one ordering rule:@@ -848,7 +1010,7 @@ elementArg :: P ElementArg elementArg =   attributePositionArg-    <|> (EArgAttr <$> try namedArg)+    <|> (EArgAttr <$> namedArg)     <|> (EArgChild <$> expr)  -- | @action(...)@ and @value(...)@, the two forms that mean something only in@@ -881,28 +1043,29 @@ -- it. actionShape :: P Attribute actionShape = do-  _ <- symbol "("+  opened <- openSym "("   event <- staticString   _ <- symbol ","   key <- staticString   _ <- symbol ","   payload <- expr-  _ <- symbol ")"+  closeSym opened ")"   pure (ActionAttr event key payload)  -- | @value(expr)@ fills the element's value slot -- see "Tramaj.Node" for -- what a host does with it. valueShape :: P Expr valueShape = do-  _ <- symbol "("+  opened <- openSym "("   v <- expr-  _ <- symbol ")"+  closeSym opened ")"   pure v +-- The 'try' covers @name:@ only, which is what tells an attribute from a+-- child; an error in its value is reported where it is. namedArg :: P Attribute namedArg = do-  name <- objectKey-  _ <- symbol ":"+  name <- try (objectKey <* symbol ":")   Attr name <$> expr  -- Precedence ---------------------------------------------------------------@@ -912,7 +1075,7 @@ expr :: P Expr expr = do   first <- operand-  rest <- many (try (symbol "<>" *> operand))+  rest <- many (symbol "<>" *> operand)   pure (foldl Concat first rest)  -- | Alternatives are ordered so that a longer form is tried before a prefix of@@ -927,7 +1090,9 @@     <|> call     <|> pathExpr     <|> allocExpr+    <|> shapeExpr     <|> demandExpr+    <|> markerExpr     <|> documentExpr     <|> stringLit     <|> numberLit@@ -939,8 +1104,11 @@ -- | @\@name=expr@ or @\@name : T = expr@ (v4-types \S7), one per line. @\@@ -- leads a binding definition, mirroring @$@ leading a binding read; the -- optional @: T@ is what tells 'SLet' and 'SAnnotate' apart.+--+-- No 'try': nothing else starts with @\@@, so an error in a binding is+-- reported where it is rather than at the start of the line. binding :: P [Stmt]-binding = try $ do+binding = do   _ <- char '@'   pat <- pattern   case pat of@@ -958,7 +1126,7 @@ -- @$@ and @\@@. A statement position only -- it may not appear inside an -- expression, so there is no operand form for it. emission :: P Expr-emission = try $ do+emission = do   _ <- char '!'   expr @@ -983,11 +1151,13 @@       root <- expr       skipSpaces       eof-      let programBody = numberAllocs (stmts (concat statements) root)+      let body = stmts (concat statements) root+      noStrayMarker body+      let programBody = numberAllocs body       pure $ case root of         Element {} -> DocumentProgram programBody         Fragment {} -> DocumentProgram programBody         _ -> ExpressionProgram programBody  parseExpr :: Text -> Either (ParseErrorBundle Text Void) Expr-parseExpr = runParser (numberAllocs <$> (skipSpaces *> expr <* eof)) ""+parseExpr = runParser (numberAllocs <$> (skipSpaces *> expr <* eof >>= \e -> e <$ noStrayMarker e)) ""
test/unit/Tramaj/AnalysisSpec.hs view
@@ -181,6 +181,10 @@   it "finds a site inside a lambda" $     symbolSites (prog "map($ctx.xs, (x) => ?($x))") `shouldBe` Set.fromList [0] +  it "reports a ?shape's site once, whatever number of markers it holds, in one sequence with ?(k)" $+    symbolSites (prog "[?shape(\"o\", {~a, b: [~b, ~c(1)], i: ?shape(\"i\", {~d})}), ?(\"p\")]")+      `shouldBe` Set.fromList [0, 1, 2]+   it "is non-empty for a program that cannot serve as a library" $     symbolSites (prog "?(\"x\")") `shouldNotBe` Set.empty 
test/unit/Tramaj/CorpusSpec.hs view
@@ -70,9 +70,16 @@ -- which is an option of each evaluation here ('optionsFromMeta'). @sort@, -- @format-number@ and @round@: transition tags for the two sort forms, the -- @format-number@ builtin and the tenth arithmetic name (decisions \S20).+-- @shape@: the @?shape(key, shape)@ form and its @~name@ markers (decisions+-- \S19). providedProfiles :: [Text]-providedProfiles = ["base", "int-float", "int64", "arithmetic", "sort", "format-number", "round"]+providedProfiles = ["base", "int-float", "int64", "arithmetic", "sort", "format-number", "round", "shape"] +-- | The profiles every port provides (@corpus/README.md@). A case naming one+-- that 'providedProfiles' lacks fails instead of being skipped.+requiredProfiles :: [Text]+requiredProfiles = ["int-float", "arithmetic"]+ -- | The profiles a case names that this port does not provide. missingProfiles :: CaseMeta -> [Text] missingProfiles = filter (`notElem` providedProfiles) . metaProfiles@@ -215,6 +222,9 @@   meta <- (readJsonFile (dir </> "meta.json") :: IO CaseMeta)   when (metaHasRequires meta) $     error (dir <> ": \"requires\" was replaced by \"profiles\" (corpus/README.md)")+  case filter (`elem` requiredProfiles) (missingProfiles meta) of+    [] -> pure ()+    required -> error (dir <> ": not provided, but required of every port: " <> T.unpack (T.intercalate ", " required))   if metaExpect meta == "analysis"     then do       -- The names are the keys of @analysis.json@, which holds no number, so
test/unit/Tramaj/ParserSpec.hs view
@@ -9,8 +9,10 @@ module Tramaj.ParserSpec (spec) where  import Data.Either (isLeft, isRight)+import Data.List (isInfixOf, isPrefixOf) import Data.Text (Text) import Test.Hspec+import Text.Megaparsec (errorBundlePretty) import Tramaj.Ast import Tramaj.Parser @@ -18,6 +20,7 @@ spec = do   acceptanceSpec   rejectionSpec+  unclosedSpec   commentSpec   desugaringSpec   programKindSpec@@ -101,6 +104,44 @@   rejects "a binding name ending in a hyphen" "@a-=1\n$a"   rejects "a tag ending in a hyphen" ".div-()" +-- | An unclosed bracket, quote or interpolation is reported where the+-- closer was looked for, and names the opener and where it is. Each case+-- gives the position the error must carry (@line:column@, as+-- 'errorBundlePretty' prints it first) and a part of its message.+--+-- Before, every one of these said line 1, column 1: the backtracking+-- alternatives that recognize a form wrapped the whole form, so the deep+-- error was dropped in favour of the alternative's starting point.+unclosedSpec :: Spec+unclosedSpec = describe "an unclosed opener" $ do+  failsAt "an element's parenthesis" ".div(\"x\"" "1:9" "')' to close the '(' of .div at line 1, column 5"+  failsAt+    "an element's parenthesis, over several lines"+    ".div(\n  .p(\"a\"),\n  .p(\"b\")\n"+    "4:1"+    "')' to close the '(' of .div at line 1, column 5"+  failsAt "a nested element's parenthesis" ".div(.p(\"a\", .span(\"b\"))" "1:25" "')' to close the '(' of .div at line 1, column 5"+  failsAt "a string" ".div(\"abc)" "1:11" "'\"' to close the string at line 1, column 6"+  failsAt "a string over several lines" ".div(class: \"x\n)" "2:2" "'\"' to close the string at line 1, column 13"+  failsAt "an interpolation" "\"a `$x" "1:7" "'`' to close the interpolation at line 1, column 4"+  failsAt "an interpolation the string closes first" ".div(\"a`$x\")" "1:11" "'`' to close the interpolation at line 1, column 8"+  failsAt "a call's parenthesis" "f(1, 2" "1:7" "')' to close the '(' at line 1, column 2"+  failsAt "a grouping parenthesis" "(1" "1:3" "')' to close the '(' at line 1, column 1"+  failsAt "an array" "[1, 2" "1:6" "']' to close the '[' at line 1, column 1"+  failsAt "an object" "{a: 1" "1:6" "'}' to close the '{' at line 1, column 1"+  failsAt "a special form's parenthesis" "map($xs, (x) => $x" "1:19" "')' to close the '(' at line 1, column 4"+  failsAt "a string in a branch arm" "branch($x, $p, \"abc" "1:20" "'\"' to close the string at line 1, column 16"+  failsAt "an array in a binding" "@x = 1\n@y = [1,\n$x" "3:3" "']' to close the '[' at line 2, column 6"+  failsAt "a static string" ".b(action(\"on-click\", \"save, {}))" "1:34" "'\"' to close the string at line 1, column 23"+  where+    failsAt :: String -> Text -> String -> String -> Spec+    failsAt name src pos msg = it name $ case parseProgram src of+      Right _ -> expectationFailure "parsed, but should not have"+      Left bundle -> do+        let rendered = errorBundlePretty bundle+        rendered `shouldSatisfy` ((pos <> ":") `isPrefixOf`)+        rendered `shouldSatisfy` (msg `isInfixOf`)+ -- | Comments: @--@ to the end of the line, single-line only. -- -- The load-bearing assertion is 'leavesNoTrace' -- a commented program and@@ -399,3 +440,38 @@   it "does not confuse !type-constraint with an ordinary !expr emission" $     parseProgram "!constraint(\"k\", 1)\ntrue"       `shouldBe` Right (ExpressionProgram (Emit (Constrain "k" [IntLit 1]) (BoolLit True)))++  -- v3-symbols \S1.2, decisions \S19: ?shape lowers at parse time.+  declares+    "?shape lowers to a hidden binding of its key and markers allocating at the form's one site"+    "?shape(\"d\", {~replicas, ports: [~http($x)]})"+    ( ExpressionProgram+        ( Let+            "#shape"+            (StringLit "d")+            ( ObjectLit+                [ ("replicas", Alloc 0 (ArrayLit [Path "#shape" [], StringLit "replicas"]))+                , ("ports", ArrayLit [Alloc 0 (ArrayLit [Path "#shape" [], StringLit "http", Path "x" []])])+                ]+            )+        )+    )++  declares+    "a ?(k) inside a shape takes the next site, after the form's own"+    "?shape(\"o\", [~a, ?(\"p\")])"+    ( ExpressionProgram+        ( Let+            "#shape"+            (StringLit "o")+            (ArrayLit [Alloc 0 (ArrayLit [Path "#shape" [], StringLit "a"]), Alloc 1 (StringLit "p")])+        )+    )++  rejects "a marker outside any shape" "[~a]"+  rejects "a marker in a ?shape's key" "?shape(~k, {~a})"+  rejects "a bare marker under a lambda inside the shape" "?shape(\"d\", map($xs, (x) => ~a))"+  rejects "a duplicate marker name, bare and keyed" "?shape(\"d\", [~a, map($xs, (x) => ~a($x))])"+  rejects "a shape with no marker" "?shape(\"d\", {a: 1})"+  rejects "?shape with three arguments" "?shape(\"d\", {~a}, 1)"+  rejects "a space after the marker leader" "?shape(\"d\", ~ a)"
tramaj-hs.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.4 name:               tramaj-hs-version:            0.4.0.0+version:            0.5.0.0 synopsis:           Haskell parser/AST/evaluator for the tramaj language description:   Parser, AST and evaluator for the tramaj language specified in@@ -81,6 +81,7 @@     , directory     , filepath     , hspec        >=2.11+    , megaparsec     , tramaj-hs     , text     , vector