jsonnet 0.1.0.0 → 0.2.0.0
raw patch · 11 files changed
+1718/−104 lines, 11 filesdep ~aesondep ~ansi-wl-pprintdep ~basenew-component:exe:hs-jsonnetPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: aeson, ansi-wl-pprint, base, bytestring, containers, mtl, optparse-applicative, template-haskell, text
API changes (from Hackage documentation)
+ Language.Jsonnet.Common: [value] :: Hideable a -> a
+ Language.Jsonnet.Common: [visiblity] :: Hideable a -> Visibility
- Language.Jsonnet: Config :: FilePath -> Value -> Config
+ Language.Jsonnet: Config :: FilePath -> Thunk -> Config
- Language.Jsonnet: [stdlib] :: Config -> Value
+ Language.Jsonnet: [stdlib] :: Config -> Thunk
Files
- README.md +106/−19
- app/Main.hs +17/−7
- jsonnet.cabal +22/−21
- src/Language/Jsonnet.hs +2/−3
- src/Language/Jsonnet/Common.hs +1/−1
- src/Language/Jsonnet/Eval.hs-boot +0/−1
- src/Language/Jsonnet/Std.hs +0/−31
- src/Language/Jsonnet/Std/Lib.hs +30/−2
- src/Language/Jsonnet/Value.hs +0/−5
- stdlib/std.jsonnet +1512/−0
- test/golden/Test.hs +28/−14
README.md view
@@ -1,7 +1,9 @@ # haskell-jsonnet -[](https://github.com/moleike/jsonnet-hs/actions) [](https://gitter.im/jsonnet-hs/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)+[](https://github.com/moleike/haskell-jsonnet/actions)+[](https://hackage.haskell.org/package/jsonnet) + A full-fledged Haskell implementation of the [Jsonnet][jsonnet] spec. For an introduction to the language itself, see the [tutorial][tutorial] or language [reference][reference]. We are using the same test suite used in the offical [C++][cpp-jsonnet] and [Go][go-jsonnet] implementation (which is fairly comprehensive).@@ -10,40 +12,122 @@ Using the [stack][stack] build tool: -```bash-git clone github.com/moleike/jsonnet-hs.git-cd jsonnet-hs-stack build+```console+% git clone github.com/moleike/haskell-jsonnet.git+% cd haskell-jsonnet+% stack build ``` +## Install++```console+% stack install # to install+```++## Usage++```console+% hs-jsonnet --help+Usage: hs-jsonnet [-v|--version] [-e|--exec] [<filename>] + [-o|--output-file <filename>] [-S|--string]++Available options:+ -v,--version Print version of the program+ -h,--help Show this help text+ -e,--exec Treat filename as code+ <filename> Jsonnet source file or stdin+ -o,--output-file <filename>+ Write to the output file rather than stdout+ -S,--string Expect a string, manifest as plain text+```++## Output formats++By default Jsonnet programs evaluate to a JSON document, serialized using `aeson`. ++The `std` library provides several methods to output other formats, e.g. +to generate a Yaml stream instead:++``` console+% hs-jsonnet -S -e "std.manifestYamlStream(['a', 1, []])"+---+"a"+---+1+---+[]+...+```++Note the we need to use the option `-S` to output a verbatim string, instead of default JSON.++Similarly, to output prettified JSON: ++``` console+% cat pretty.jsonnet +std.manifestJsonEx(+{+ x: [1, 2, 3, true, false, null,+ "string\nstring"],+ y: { a: 1, b: 2, c: [1, 2] },+}, " ")++% hs-jsonnet -S pretty.jsonnet+{+ "x": [+ 1,+ 2,+ 3,+ true,+ false,+ null,+ "string\nstring"+ ],+ "y": {+ "a": 1,+ "b": 2,+ "c": [+ 1,+ 2+ ]+ }+}+```++See the Standard library [documentation][stdlib] for more details.+ ## Progress Here is the implementation status of the main language features: - [X] array and object comprehension - [X] array slices-- [X] Python-style string formatting -- [X] text blocks -- [X] verbatim strings -- [X] object-level locals -- [ ] object-level asserts -- [X] keyword parameters -- [X] default arguments -- [ ] top-level arguments -- [ ] external variables +- [X] Python-style string formatting+- [X] text blocks+- [X] verbatim strings+- [X] object-level locals+- [ ] object-level asserts+- [X] keyword parameters+- [X] default arguments+- [ ] top-level arguments+- [ ] external variables - [X] hidden fields ([@CristhianMotoche](https://github.com/CristhianMotoche)) -- [X] tailstrict annotation +- [X] tailstrict annotation OO features are implemented but need some more work:+ - [X] `self` keyword - [X] `super` keyword-- [X] outermost object reference `$` +- [X] outermost object reference `$` - [X] object composition (merging objects)-- [X] field composition (`+:` field syntax) +- [X] field composition (`+:` field syntax) [//]: # "Implementation overview" +## Benchmarks +Preliminary [results][benchmark-gist] using the benchmarks [here][cpp-benchmarks] +for comparison. ## Contributing @@ -51,7 +135,8 @@ ## Acknowledgments -I took inspiration from [Expresso][Expresso], [hnix][hnix], [fixplate][fixplate], and numerous other libraries. Thanks to their authors.+I took inspiration from [Expresso][Expresso], [hnix][hnix], [fixplate][fixplate], +and numerous other libraries. Thanks to their authors. ## License @@ -69,5 +154,7 @@ [contributing]: https://github.com/moleike/haskell-jsonnet/blob/master/CONTRIBUTING.md [license]: https://github.com/moleike/haskell-jsonnet/blob/master/LICENSE [cpp-jsonnet]: https://github.com/google/jsonnet+[cpp-benchmarks]: https://github.com/google/jsonnet/tree/master/benchmarks [go-jsonnet]: https://github.com/google/go-jsonnet-+[benchmark-gist]: https://gist.github.com/moleike/17d5de15be06b05ddad317fe1fcf95a5+[stdlib]: https://jsonnet.org/ref/stdlib.html
app/Main.hs view
@@ -18,8 +18,15 @@ import qualified Data.Text.IO as TIO import Data.Version (showVersion) import Language.Jsonnet+import Language.Jsonnet.Annotate+import Language.Jsonnet.Desugar import Language.Jsonnet.Error-import Language.Jsonnet.Std+import Language.Jsonnet.Eval+import Language.Jsonnet.Eval (mergeWith)+import Language.Jsonnet.Eval.Monad+import qualified Language.Jsonnet.Std.Lib as Lib+import Language.Jsonnet.Std.TH (mkStdlib)+import Language.Jsonnet.Value import Options.Applicative import Paths_jsonnet (version) import Text.PrettyPrint.ANSI.Leijen (Pretty, pretty)@@ -61,19 +68,22 @@ FileInput path -> TIO.readFile path ExecInput str -> pure (T.pack str) -getStdlib :: IO Value-getStdlib =- runExceptT std >>= \case- Right stdlib -> pure stdlib- Left err -> error (show $ pretty err)+-- the jsonnet stdlib is written in both jsonnet and Haskell, here we merge+-- the native (a small subset) with the interpreted (the splice mkStdlib) +std :: Eval Value+std = eval core >>= flip mergeObjects Lib.std+ where+ core = desugar (annMap (const ()) $mkStdlib)+ mergeObjects (VObj x) (VObj y) = pure $ VObj (x `mergeWith` y)+ mkConfig :: Options -> IO Config mkConfig Options {..} = do let fname = case input of Stdin -> "" FileInput path -> path ExecInput _ -> ""- stdlib <- getStdlib+ stdlib <- mkThunk std pure Config {..} fileOutput :: Parser Output
jsonnet.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: jsonnet-version: 0.1.0.0+version: 0.2.0.0 synopsis: Jsonnet implementaton in pure Haskell description: Please see the README on GitHub at <https://github.com/moleike/jsonnet-haskell#readme> homepage: https://github.com/moleike/haskell-jsonnet#readme@@ -16,6 +16,7 @@ extra-source-files: README.md CHANGELOG.md+ stdlib/std.jsonnet source-repository head type: git@@ -30,20 +31,19 @@ , Language.Jsonnet.Parser.SrcSpan , Language.Jsonnet.Syntax , Language.Jsonnet.Syntax.Annotated- , Language.Jsonnet.Core- , Language.Jsonnet.Check- , Language.Jsonnet.Desugar , Language.Jsonnet.Annotate , Language.Jsonnet.Common- , Language.Jsonnet.Eval- , Language.Jsonnet.Eval.Monad , Language.Jsonnet.Value- , Language.Jsonnet.Std , Language.Jsonnet.Std.Lib , Language.Jsonnet.Std.TH- , Language.Jsonnet.Manifest , Language.Jsonnet.TH , Language.Jsonnet.TH.QQ+ , Language.Jsonnet.Eval+ , Language.Jsonnet.Eval.Monad+ , Language.Jsonnet.Core+ , Language.Jsonnet.Check+ , Language.Jsonnet.Desugar+ , Language.Jsonnet.Manifest autogen-modules: Paths_jsonnet other-modules:@@ -53,13 +53,13 @@ src build-depends: aeson >= 1.5.6 && < 1.6,- base >= 4.14.1 && < 4.15,+ base >= 4.15.0 && < 4.16, bytestring >= 0.10.12 && < 0.11,- containers >= 0.6.2 && < 0.7,+ containers >= 0.6.4 && < 0.7, scientific >= 0.3.6 && < 0.4, hashable >= 1.3.1 && < 1.4, text >= 1.2.4 && < 1.3,- template-haskell >= 2.16.0 && < 2.17,+ template-haskell >= 2.17.0 && < 2.18, data-fix >= 0.3.1 && < 0.4, transformers-compat >= 0.6.6 && < 0.7, unordered-containers >= 0.2.13 && < 0.3,@@ -73,7 +73,8 @@ megaparsec >= 9.0.1 && < 9.1, parser-combinators >= 1.3.0 && < 1.4, semigroupoids >= 5.3.5 && < 5.4,- unbound-generics >= 0.4.1 && < 0.5+ unbound-generics >= 0.4.1 && < 0.5,+ optparse-applicative >= 0.16.1 && < 0.17 default-language: Haskell2010 default-extensions: MultiParamTypeClasses@@ -82,7 +83,7 @@ , DeriveGeneric , LambdaCase -executable jsonnet+executable hs-jsonnet main-is: Main.hs other-modules: Paths_jsonnet@@ -90,14 +91,14 @@ app ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends:- jsonnet,- base >= 4.14.1 && < 4.15,- optparse-applicative >= 0.16.1 && < 0.17,- text >= 1.2.4 && < 1.3,- ansi-wl-pprint >= 0.6.9 && < 0.7,- mtl >= 2.2.2 && < 2.3,- bytestring >= 0.10.12 && < 0.11,- aeson >= 1.5.6 && < 1.6+ jsonnet+ , base+ , optparse-applicative+ , text+ , ansi-wl-pprint+ , mtl+ , bytestring+ , aeson default-language: Haskell2010 test-suite jsonnet-test
src/Language/Jsonnet.hs view
@@ -12,7 +12,6 @@ Value (..), runJsonnetM, parse,- desugar, evaluate, ) where@@ -58,7 +57,7 @@ data Config = Config { fname :: FilePath,- stdlib :: Value+ stdlib :: Thunk } runJsonnetM :: Config -> JsonnetM a -> IO (Either Error a)@@ -91,7 +90,7 @@ -- evaluate a Core expression with the implicit stdlib evaluate :: Core -> JsonnetM JSON.Value evaluate expr = do- ctx <- singleton "std" . TV . pure <$> asks stdlib+ ctx <- singleton "std" <$> asks stdlib JsonnetM $ lift $ runEval (emptyEnv {ctx = ctx}) ((eval >=> manifest) expr)
src/Language/Jsonnet/Common.hs view
@@ -190,7 +190,7 @@ forced :: a -> Bool hidden :: a -> Bool -data Hideable a = Hideable a Visibility+data Hideable a = Hideable {value :: a, visiblity :: Visibility} deriving ( Eq, Read,
src/Language/Jsonnet/Eval.hs-boot view
@@ -6,5 +6,4 @@ import {-# SOURCE #-} Language.Jsonnet.Value (Thunk, Value) eval :: Core -> Eval Value- evalClos :: Ctx -> Fun -> [Arg Thunk] -> Eval Value
− src/Language/Jsonnet/Std.hs
@@ -1,31 +0,0 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskell #-}--module Language.Jsonnet.Std- ( std,- )-where--import Control.Monad.Except-import Language.Jsonnet.Annotate-import Language.Jsonnet.Desugar-import Language.Jsonnet.Error-import Language.Jsonnet.Eval-import Language.Jsonnet.Eval (mergeWith)-import Language.Jsonnet.Eval.Monad-import qualified Language.Jsonnet.Std.Lib as Lib-import Language.Jsonnet.Std.TH (mkStdlib)-import Language.Jsonnet.Value-import Prelude hiding (length)---- the jsonnet stdlib is written in both jsonnet and Haskell, here we merge--- the native (small, Haskell) with the interpreted (the splice mkStdlib)-std :: ExceptT Error IO Value-std = do- ast <- pure $(mkStdlib)- core <- pure $ desugar (annMap (const ()) ast)- runEval emptyEnv (eval core >>= flip mergeObjects Lib.std)- where- mergeObjects (VObj x) (VObj y) = pure $ VObj (x `mergeWith` y)
src/Language/Jsonnet/Std/Lib.hs view
@@ -15,6 +15,7 @@ import Data.Aeson (FromJSON (..)) import qualified Data.Aeson as JSON import qualified Data.ByteString as B+import Data.Foldable (foldrM) import Data.HashMap.Lazy (HashMap) import qualified Data.HashMap.Lazy as H import Data.List (sort)@@ -46,6 +47,7 @@ (\(k, v) -> (k, pure v)) [ ("type", inj valueType), ("primitiveEquals", inj primitiveEquals),+ ("equals", inj equals), ("length", inj length), ("pow", inj ((^^) @Double @Int)), ("exp", inj (exp @Double)),@@ -78,14 +80,37 @@ primitiveEquals (VBool a) (VBool b) = pure (a == b) primitiveEquals (VStr a) (VStr b) = pure (a == b) primitiveEquals (VNum a) (VNum b) = pure (a == b)-primitiveEquals _ _ =+primitiveEquals a b = throwE ( StdError $ text $ T.unpack $- "primitiveEquals operates on primitive types"+ "primitiveEquals operates on primitive types "+ <> valueType a+ <> valueType b ) +equals :: Value -> Value -> Eval Bool+equals as@(VArr a) bs@(VArr b)+ | P.length a == P.length b = do+ as' <- proj as+ bs' <- proj bs+ allM (uncurry equals) (zip as' bs')+ | P.length a /= P.length b = pure False+equals (VObj a) (VObj b) = do+ let fields = objectFieldsEx a False+ if fields /= objectFieldsEx b False+ then pure False+ else allM objectFieldEquals fields+ where+ objectFieldEquals field = do+ a' <- force (value $ a H.! field)+ b' <- force (value $ b H.! field)+ equals a' b'+equals a b+ | valueType a == valueType b = primitiveEquals a b+equals _ _ = pure False+ objectFieldsEx :: Object -> Bool -> [Text] objectFieldsEx o True = sort (H.keys o) -- all fields objectFieldsEx o False = sort $ H.keys $ H.filter (not . hidden) o -- only visible (incl. forced)@@ -118,6 +143,9 @@ thisFile = f <$> gets currentPos where f = fmap (takeFileName . sourceName . spanBegin)++allM :: Monad m => (a -> m Bool) -> [a] -> m Bool+allM p = foldrM (\a b -> (&& b) <$> p a) True instance FromJSON Value where parseJSON = \case
src/Language/Jsonnet/Value.hs view
@@ -166,11 +166,6 @@ proj v = throwTypeMismatch "object" v inj = VObj ---instance HasValue a => HasValue (Object a) where--- proj (VObj o) = traverse proj' o--- proj v = throwTypeMismatch "object" v--- inj o = VObj $ mkThunk' . inj <$> o- instance {-# OVERLAPS #-} (HasValue a, HasValue b) => HasValue (a -> b) where proj v = throwTypeMismatch "impossible" v inj f = VFun $ \x -> force x >>= fmap (inj . f) . proj
+ stdlib/std.jsonnet view
@@ -0,0 +1,1512 @@+/*+Copyright 2015 Google Inc. All rights reserved.++Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.+*/++/* This is the Jsonnet standard library, at least the parts of it that are written in Jsonnet.+ *+ * There are some native methods as well, which are defined in the interpreter and added to this+ * file. It is never necessary to import std.jsonnet, it is embedded into the interpreter at+ * compile-time and automatically imported into all other Jsonnet programs.+ */+{++ local std = self,+ local id = function(x) x,++ isString(v):: std.type(v) == 'string',+ isNumber(v):: std.type(v) == 'number',+ isBoolean(v):: std.type(v) == 'boolean',+ isObject(v):: std.type(v) == 'object',+ isArray(v):: std.type(v) == 'array',+ isFunction(v):: std.type(v) == 'function',++ toString(a)::+ if std.type(a) == 'string' then a else '' + a,++ substr(str, from, len)::+ assert std.isString(str) : 'substr first parameter should be a string, got ' + std.type(str);+ assert std.isNumber(from) : 'substr second parameter should be a string, got ' + std.type(from);+ assert std.isNumber(len) : 'substr third parameter should be a string, got ' + std.type(len);+ assert len >= 0 : 'substr third parameter should be greater than zero, got ' + len;+ std.join('', std.makeArray(std.max(0, std.min(len, std.length(str) - from)), function(i) str[i + from])),++ startsWith(a, b)::+ if std.length(a) < std.length(b) then+ false+ else+ std.substr(a, 0, std.length(b)) == b,++ endsWith(a, b)::+ if std.length(a) < std.length(b) then+ false+ else+ std.substr(a, std.length(a) - std.length(b), std.length(b)) == b,++ lstripChars(str, chars)::+ if std.length(str) > 0 && std.member(chars, str[0]) then+ std.lstripChars(str[1:], chars)+ else+ str,++ rstripChars(str, chars)::+ local len = std.length(str);+ if len > 0 && std.member(chars, str[len - 1]) then+ std.rstripChars(str[:len - 1], chars)+ else+ str,++ stripChars(str, chars)::+ std.lstripChars(std.rstripChars(str, chars), chars),++ stringChars(str)::+ std.makeArray(std.length(str), function(i) str[i]),++ local parse_nat(str, base) =+ assert base > 0 && base <= 16 : 'integer base %d invalid' % base;+ // These codepoints are in ascending order:+ local zero_code = std.codepoint('0');+ local upper_a_code = std.codepoint('A');+ local lower_a_code = std.codepoint('a');+ local addDigit(aggregate, char) =+ local code = std.codepoint(char);+ local digit = if code >= lower_a_code then+ code - lower_a_code + 10+ else if code >= upper_a_code then+ code - upper_a_code + 10+ else+ code - zero_code;+ assert digit >= 0 && digit < base : '%s is not a base %d integer' % [str, base];+ base * aggregate + digit;+ std.foldl(addDigit, std.stringChars(str), 0),++ parseInt(str)::+ assert std.isString(str) : 'Expected string, got ' + std.type(str);+ assert std.length(str) > 0 && str != '-' : 'Not an integer: "%s"' % [str];+ if str[0] == '-' then+ -parse_nat(str[1:], 10)+ else+ parse_nat(str, 10),++ parseOctal(str)::+ assert std.isString(str) : 'Expected string, got ' + std.type(str);+ assert std.length(str) > 0 : 'Not an octal number: ""';+ parse_nat(str, 8),++ parseHex(str)::+ assert std.isString(str) : 'Expected string, got ' + std.type(str);+ assert std.length(str) > 0 : 'Not hexadecimal: ""';+ parse_nat(str, 16),++ split(str, c)::+ assert std.isString(str) : 'std.split first parameter should be a string, got ' + std.type(str);+ assert std.isString(c) : 'std.split second parameter should be a string, got ' + std.type(c);+ assert std.length(c) == 1 : 'std.split second parameter should have length 1, got ' + std.length(c);+ std.splitLimit(str, c, -1),++ splitLimit(str, c, maxsplits)::+ assert std.isString(str) : 'std.splitLimit first parameter should be a string, got ' + std.type(str);+ assert std.isString(c) : 'std.splitLimit second parameter should be a string, got ' + std.type(c);+ assert std.length(c) == 1 : 'std.splitLimit second parameter should have length 1, got ' + std.length(c);+ assert std.isNumber(maxsplits) : 'std.splitLimit third parameter should be a number, got ' + std.type(maxsplits);+ local aux(str, delim, i, arr, v) =+ local c = str[i];+ local i2 = i + 1;+ if i >= std.length(str) then+ arr + [v]+ else if c == delim && (maxsplits == -1 || std.length(arr) < maxsplits) then+ aux(str, delim, i2, arr + [v], '') tailstrict+ else+ aux(str, delim, i2, arr, v + c) tailstrict;+ aux(str, c, 0, [], ''),++ strReplace(str, from, to)::+ assert std.isString(str);+ assert std.isString(from);+ assert std.isString(to);+ assert from != '' : "'from' string must not be zero length.";++ // Cache for performance.+ local str_len = std.length(str);+ local from_len = std.length(from);++ // True if from is at str[i].+ local found_at(i) = str[i:i + from_len] == from;++ // Return the remainder of 'str' starting with 'start_index' where+ // all occurrences of 'from' after 'curr_index' are replaced with 'to'.+ local replace_after(start_index, curr_index, acc) =+ if curr_index > str_len then+ acc + str[start_index:curr_index]+ else if found_at(curr_index) then+ local new_index = curr_index + std.length(from);+ replace_after(new_index, new_index, acc + str[start_index:curr_index] + to) tailstrict+ else+ replace_after(start_index, curr_index + 1, acc) tailstrict;++ // if from_len==1, then we replace by splitting and rejoining the+ // string which is much faster than recursing on replace_after+ if from_len == 1 then+ std.join(to, std.split(str, from))+ else+ replace_after(0, 0, ''),++ asciiUpper(str)::+ local cp = std.codepoint;+ local up_letter(c) = if cp(c) >= 97 && cp(c) < 123 then+ std.char(cp(c) - 32)+ else+ c;+ std.join('', std.map(up_letter, std.stringChars(str))),++ asciiLower(str)::+ local cp = std.codepoint;+ local down_letter(c) = if cp(c) >= 65 && cp(c) < 91 then+ std.char(cp(c) + 32)+ else+ c;+ std.join('', std.map(down_letter, std.stringChars(str))),++ range(from, to)::+ std.makeArray(to - from + 1, function(i) i + from),++ repeat(what, count)::+ local joiner =+ if std.isString(what) then ''+ else if std.isArray(what) then []+ else error 'std.repeat first argument must be an array or a string';+ std.join(joiner, std.makeArray(count, function(i) what)),++ slice(indexable, index, end, step)::+ local invar =+ // loop invariant with defaults applied+ {+ indexable: indexable,+ index:+ if index == null then 0+ else index,+ end:+ if end == null then std.length(indexable)+ else end,+ step:+ if step == null then 1+ else step,+ length: std.length(indexable),+ type: std.type(indexable),+ };+ assert invar.index >= 0 && invar.end >= 0 && invar.step >= 0 : 'got [%s:%s:%s] but negative index, end, and steps are not supported' % [invar.index, invar.end, invar.step];+ assert step != 0 : 'got %s but step must be greater than 0' % step;+ assert std.isString(indexable) || std.isArray(indexable) : 'std.slice accepts a string or an array, but got: %s' % std.type(indexable);+ local build(slice, cur) =+ if cur >= invar.end || cur >= invar.length then+ slice+ else+ build(+ if invar.type == 'string' then+ slice + invar.indexable[cur]+ else+ slice + [invar.indexable[cur]],+ cur + invar.step+ ) tailstrict;+ build(if invar.type == 'string' then '' else [], invar.index),++ member(arr, x)::+ if std.isArray(arr) then+ std.count(arr, x) > 0+ else if std.isString(arr) then+ std.length(std.findSubstr(x, arr)) > 0+ else error 'std.member first argument must be an array or a string',++ count(arr, x):: std.length(std.filter(function(v) v == x, arr)),++ mod(a, b)::+ if std.isNumber(a) && std.isNumber(b) then+ std.modulo(a, b)+ else if std.isString(a) then+ std.format(a, b)+ else+ error 'Operator % cannot be used on types ' + std.type(a) + ' and ' + std.type(b) + '.',++ map(func, arr)::+ if !std.isFunction(func) then+ error ('std.map first param must be function, got ' + std.type(func))+ else if !std.isArray(arr) && !std.isString(arr) then+ error ('std.map second param must be array / string, got ' + std.type(arr))+ else+ std.makeArray(std.length(arr), function(i) func(arr[i])),++ mapWithIndex(func, arr)::+ if !std.isFunction(func) then+ error ('std.mapWithIndex first param must be function, got ' + std.type(func))+ else if !std.isArray(arr) && !std.isString(arr) then+ error ('std.mapWithIndex second param must be array, got ' + std.type(arr))+ else+ std.makeArray(std.length(arr), function(i) func(i, arr[i])),++ mapWithKey(func, obj)::+ if !std.isFunction(func) then+ error ('std.mapWithKey first param must be function, got ' + std.type(func))+ else if !std.isObject(obj) then+ error ('std.mapWithKey second param must be object, got ' + std.type(obj))+ else+ { [k]: func(k, obj[k]) for k in std.objectFields(obj) },++ flatMap(func, arr)::+ if !std.isFunction(func) then+ error ('std.flatMap first param must be function, got ' + std.type(func))+ else if std.isArray(arr) then+ std.flattenArrays(std.makeArray(std.length(arr), function(i) func(arr[i])))+ else if std.isString(arr) then+ std.join('', std.makeArray(std.length(arr), function(i) func(arr[i])))+ else error ('std.flatMap second param must be array / string, got ' + std.type(arr)),++ join(sep, arr)::+ local aux(arr, i, first, running) =+ if i >= std.length(arr) then+ running+ else if arr[i] == null then+ aux(arr, i + 1, first, running) tailstrict+ else if std.type(arr[i]) != std.type(sep) then+ error 'expected %s but arr[%d] was %s ' % [std.type(sep), i, std.type(arr[i])]+ else if first then+ aux(arr, i + 1, false, running + arr[i]) tailstrict+ else+ aux(arr, i + 1, false, running + sep + arr[i]) tailstrict;+ if !std.isArray(arr) then+ error 'join second parameter should be array, got ' + std.type(arr)+ else if std.isString(sep) then+ aux(arr, 0, true, '')+ else if std.isArray(sep) then+ aux(arr, 0, true, [])+ else+ error 'join first parameter should be string or array, got ' + std.type(sep),++ lines(arr)::+ std.join('\n', arr + ['']),++ deepJoin(arr)::+ if std.isString(arr) then+ arr+ else if std.isArray(arr) then+ std.join('', [std.deepJoin(x) for x in arr])+ else+ error 'Expected string or array, got %s' % std.type(arr),+++ format(str, vals)::++ /////////////////////////////+ // Parse the mini-language //+ /////////////////////////////++ local try_parse_mapping_key(str, i) =+ assert i < std.length(str) : 'Truncated format code.';+ local c = str[i];+ if c == '(' then+ local consume(str, j, v) =+ if j >= std.length(str) then+ error 'Truncated format code.'+ else+ local c = str[j];+ if c != ')' then+ consume(str, j + 1, v + c)+ else+ { i: j + 1, v: v };+ consume(str, i + 1, '')+ else+ { i: i, v: null };++ local try_parse_cflags(str, i) =+ local consume(str, j, v) =+ assert j < std.length(str) : 'Truncated format code.';+ local c = str[j];+ if c == '#' then+ consume(str, j + 1, v { alt: true })+ else if c == '0' then+ consume(str, j + 1, v { zero: true })+ else if c == '-' then+ consume(str, j + 1, v { left: true })+ else if c == ' ' then+ consume(str, j + 1, v { blank: true })+ else if c == '+' then+ consume(str, j + 1, v { plus: true })+ else+ { i: j, v: v };+ consume(str, i, { alt: false, zero: false, left: false, blank: false, plus: false });++ local try_parse_field_width(str, i) =+ if i < std.length(str) && str[i] == '*' then+ { i: i + 1, v: '*' }+ else+ local consume(str, j, v) =+ assert j < std.length(str) : 'Truncated format code.';+ local c = str[j];+ if c == '0' then+ consume(str, j + 1, v * 10 + 0)+ else if c == '1' then+ consume(str, j + 1, v * 10 + 1)+ else if c == '2' then+ consume(str, j + 1, v * 10 + 2)+ else if c == '3' then+ consume(str, j + 1, v * 10 + 3)+ else if c == '4' then+ consume(str, j + 1, v * 10 + 4)+ else if c == '5' then+ consume(str, j + 1, v * 10 + 5)+ else if c == '6' then+ consume(str, j + 1, v * 10 + 6)+ else if c == '7' then+ consume(str, j + 1, v * 10 + 7)+ else if c == '8' then+ consume(str, j + 1, v * 10 + 8)+ else if c == '9' then+ consume(str, j + 1, v * 10 + 9)+ else+ { i: j, v: v };+ consume(str, i, 0);++ local try_parse_precision(str, i) =+ assert i < std.length(str) : 'Truncated format code.';+ local c = str[i];+ if c == '.' then+ try_parse_field_width(str, i + 1)+ else+ { i: i, v: null };++ // Ignored, if it exists.+ local try_parse_length_modifier(str, i) =+ assert i < std.length(str) : 'Truncated format code.';+ local c = str[i];+ if c == 'h' || c == 'l' || c == 'L' then+ i + 1+ else+ i;++ local parse_conv_type(str, i) =+ assert i < std.length(str) : 'Truncated format code.';+ local c = str[i];+ if c == 'd' || c == 'i' || c == 'u' then+ { i: i + 1, v: 'd', caps: false }+ else if c == 'o' then+ { i: i + 1, v: 'o', caps: false }+ else if c == 'x' then+ { i: i + 1, v: 'x', caps: false }+ else if c == 'X' then+ { i: i + 1, v: 'x', caps: true }+ else if c == 'e' then+ { i: i + 1, v: 'e', caps: false }+ else if c == 'E' then+ { i: i + 1, v: 'e', caps: true }+ else if c == 'f' then+ { i: i + 1, v: 'f', caps: false }+ else if c == 'F' then+ { i: i + 1, v: 'f', caps: true }+ else if c == 'g' then+ { i: i + 1, v: 'g', caps: false }+ else if c == 'G' then+ { i: i + 1, v: 'g', caps: true }+ else if c == 'c' then+ { i: i + 1, v: 'c', caps: false }+ else if c == 's' then+ { i: i + 1, v: 's', caps: false }+ else if c == '%' then+ { i: i + 1, v: '%', caps: false }+ else+ error 'Unrecognised conversion type: ' + c;+++ // Parsed initial %, now the rest.+ local parse_code(str, i) =+ assert i < std.length(str) : 'Truncated format code.';+ local mkey = try_parse_mapping_key(str, i);+ local cflags = try_parse_cflags(str, mkey.i);+ local fw = try_parse_field_width(str, cflags.i);+ local prec = try_parse_precision(str, fw.i);+ local len_mod = try_parse_length_modifier(str, prec.i);+ local ctype = parse_conv_type(str, len_mod);+ {+ i: ctype.i,+ code: {+ mkey: mkey.v,+ cflags: cflags.v,+ fw: fw.v,+ prec: prec.v,+ ctype: ctype.v,+ caps: ctype.caps,+ },+ };++ // Parse a format string (containing none or more % format tags).+ local parse_codes(str, i, out, cur) =+ if i >= std.length(str) then+ out + [cur]+ else+ local c = str[i];+ if c == '%' then+ local r = parse_code(str, i + 1);+ parse_codes(str, r.i, out + [cur, r.code], '') tailstrict+ else+ parse_codes(str, i + 1, out, cur + c) tailstrict;++ local codes = parse_codes(str, 0, [], '');+++ ///////////////////////+ // Format the values //+ ///////////////////////++ // Useful utilities+ local padding(w, s) =+ local aux(w, v) =+ if w <= 0 then+ v+ else+ aux(w - 1, v + s);+ aux(w, '');++ // Add s to the left of str so that its length is at least w.+ local pad_left(str, w, s) =+ padding(w - std.length(str), s) + str;++ // Add s to the right of str so that its length is at least w.+ local pad_right(str, w, s) =+ str + padding(w - std.length(str), s);++ // Render a sign & magnitude integer (radix ranges from decimal to binary).+ // neg should be a boolean, and when true indicates that we should render a negative number.+ // mag must always be a whole number >= 0, it's the magnitude of the integer to render+ // min_chars must be a whole number >= 0+ // It is the field width, i.e. std.length() of the result should be >= min_chars+ // min_digits must be a whole number >= 0. It's the number of zeroes to pad with.+ // blank must be a boolean, if true adds an additional ' ' in front of a positive number, so+ // that it is aligned with negative numbers with the same number of digits.+ // plus must be a boolean, if true adds a '+' in front of a postive number, so that it is+ // aligned with negative numbers with the same number of digits. This takes precedence over+ // blank, if both are true.+ // radix must be a whole number >1 and <= 10. It is the base of the system of numerals.+ // zero_prefix is a string prefixed before the sign to all numbers that are not 0.+ local render_int(neg, mag, min_chars, min_digits, blank, plus, radix, zero_prefix) =+ // dec is the minimal string needed to represent the number as text.+ local dec =+ if mag == 0 then+ '0'+ else+ local aux(n) =+ if n == 0 then+ zero_prefix+ else+ aux(std.floor(n / radix)) + (n % radix);+ aux(mag);+ local zp = min_chars - (if neg || blank || plus then 1 else 0);+ local zp2 = std.max(zp, min_digits);+ local dec2 = pad_left(dec, zp2, '0');+ (if neg then '-' else if plus then '+' else if blank then ' ' else '') + dec2;++ // Render an integer in hexadecimal.+ local render_hex(n__, min_chars, min_digits, blank, plus, add_zerox, capitals) =+ local numerals = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]+ + if capitals then ['A', 'B', 'C', 'D', 'E', 'F']+ else ['a', 'b', 'c', 'd', 'e', 'f'];+ local n_ = std.abs(n__);+ local aux(n) =+ if n == 0 then+ ''+ else+ aux(std.floor(n / 16)) + numerals[n % 16];+ local hex = if std.floor(n_) == 0 then '0' else aux(std.floor(n_));+ local neg = n__ < 0;+ local zp = min_chars - (if neg || blank || plus then 1 else 0)+ - (if add_zerox then 2 else 0);+ local zp2 = std.max(zp, min_digits);+ local hex2 = (if add_zerox then (if capitals then '0X' else '0x') else '')+ + pad_left(hex, zp2, '0');+ (if neg then '-' else if plus then '+' else if blank then ' ' else '') + hex2;++ local strip_trailing_zero(str) =+ local aux(str, i) =+ if i < 0 then+ ''+ else+ if str[i] == '0' then+ aux(str, i - 1)+ else+ std.substr(str, 0, i + 1);+ aux(str, std.length(str) - 1);++ // Render floating point in decimal form+ local render_float_dec(n__, zero_pad, blank, plus, ensure_pt, trailing, prec) =+ local n_ = std.abs(n__);+ local whole = std.floor(n_);+ // Represent the rounded number as an integer * 1/10**prec.+ // Note that it can also be equal to 10**prec and we'll need to carry+ // over to the wholes. We operate on the absolute numbers, so that we+ // don't have trouble with the rounding direction.+ local denominator = std.pow(10, prec);+ local numerator = std.abs(n_) * denominator + 0.5;+ local whole = std.sign(n_) * std.floor(numerator / denominator);+ local frac = std.floor(numerator) % denominator;+ local dot_size = if prec == 0 && !ensure_pt then 0 else 1;+ local zp = zero_pad - prec - dot_size;+ local str = render_int(n__ < 0, whole, zp, 0, blank, plus, 10, '');+ if prec == 0 then+ str + if ensure_pt then '.' else ''+ else+ if trailing || frac > 0 then+ local frac_str = render_int(false, frac, prec, 0, false, false, 10, '');+ str + '.' + if !trailing then strip_trailing_zero(frac_str) else frac_str+ else+ str;++ // Render floating point in scientific form+ local render_float_sci(n__, zero_pad, blank, plus, ensure_pt, trailing, caps, prec) =+ local exponent = if n__ == 0 then 0 else std.floor(std.log(std.abs(n__)) / std.log(10));+ local suff = (if caps then 'E' else 'e')+ + render_int(exponent < 0, std.abs(exponent), 3, 0, false, true, 10, '');+ local mantissa = if exponent == -324 then+ // Avoid a rounding error where std.pow(10, -324) is 0+ // -324 is the smallest exponent possible.+ n__ * 10 / std.pow(10, exponent + 1)+ else+ n__ / std.pow(10, exponent);+ local zp2 = zero_pad - std.length(suff);+ render_float_dec(mantissa, zp2, blank, plus, ensure_pt, trailing, prec) + suff;++ // Render a value with an arbitrary format code.+ local format_code(val, code, fw, prec_or_null, i) =+ local cflags = code.cflags;+ local fpprec = if prec_or_null != null then prec_or_null else 6;+ local iprec = if prec_or_null != null then prec_or_null else 0;+ local zp = if cflags.zero && !cflags.left then fw else 0;+ if code.ctype == 's' then+ std.toString(val)+ else if code.ctype == 'd' then+ if std.type(val) != 'number' then+ error 'Format required number at '+ + i + ', got ' + std.type(val)+ else+ render_int(val <= -1, std.floor(std.abs(val)), zp, iprec, cflags.blank, cflags.plus, 10, '')+ else if code.ctype == 'o' then+ if std.type(val) != 'number' then+ error 'Format required number at '+ + i + ', got ' + std.type(val)+ else+ local zero_prefix = if cflags.alt then '0' else '';+ render_int(val <= -1, std.floor(std.abs(val)), zp, iprec, cflags.blank, cflags.plus, 8, zero_prefix)+ else if code.ctype == 'x' then+ if std.type(val) != 'number' then+ error 'Format required number at '+ + i + ', got ' + std.type(val)+ else+ render_hex(std.floor(val),+ zp,+ iprec,+ cflags.blank,+ cflags.plus,+ cflags.alt,+ code.caps)+ else if code.ctype == 'f' then+ if std.type(val) != 'number' then+ error 'Format required number at '+ + i + ', got ' + std.type(val)+ else+ render_float_dec(val,+ zp,+ cflags.blank,+ cflags.plus,+ cflags.alt,+ true,+ fpprec)+ else if code.ctype == 'e' then+ if std.type(val) != 'number' then+ error 'Format required number at '+ + i + ', got ' + std.type(val)+ else+ render_float_sci(val,+ zp,+ cflags.blank,+ cflags.plus,+ cflags.alt,+ true,+ code.caps,+ fpprec)+ else if code.ctype == 'g' then+ if std.type(val) != 'number' then+ error 'Format required number at '+ + i + ', got ' + std.type(val)+ else+ local exponent = std.floor(std.log(std.abs(val)) / std.log(10));+ if exponent < -4 || exponent >= fpprec then+ render_float_sci(val,+ zp,+ cflags.blank,+ cflags.plus,+ cflags.alt,+ cflags.alt,+ code.caps,+ fpprec - 1)+ else+ local digits_before_pt = std.max(1, exponent + 1);+ render_float_dec(val,+ zp,+ cflags.blank,+ cflags.plus,+ cflags.alt,+ cflags.alt,+ fpprec - digits_before_pt)+ else if code.ctype == 'c' then+ if std.type(val) == 'number' then+ std.char(val)+ else if std.type(val) == 'string' then+ if std.length(val) == 1 then+ val+ else+ error '%c expected 1-sized string got: ' + std.length(val)+ else+ error '%c expected number / string, got: ' + std.type(val)+ else+ error 'Unknown code: ' + code.ctype;++ // Render a parsed format string with an array of values.+ local format_codes_arr(codes, arr, i, j, v) =+ if i >= std.length(codes) then+ if j < std.length(arr) then+ error ('Too many values to format: ' + std.length(arr) + ', expected ' + j)+ else+ v+ else+ local code = codes[i];+ if std.type(code) == 'string' then+ format_codes_arr(codes, arr, i + 1, j, v + code) tailstrict+ else+ local tmp = if code.fw == '*' then {+ j: j + 1,+ fw: if j >= std.length(arr) then+ error ('Not enough values to format: ' + std.length(arr) + ', expected at least ' + j)+ else+ arr[j],+ } else {+ j: j,+ fw: code.fw,+ };+ local tmp2 = if code.prec == '*' then {+ j: tmp.j + 1,+ prec: if tmp.j >= std.length(arr) then+ error ('Not enough values to format: ' + std.length(arr) + ', expected at least ' + tmp.j)+ else+ arr[tmp.j],+ } else {+ j: tmp.j,+ prec: code.prec,+ };+ local j2 = tmp2.j;+ local val =+ if j2 < std.length(arr) then+ arr[j2]+ else+ error ('Not enough values to format: ' + std.length(arr) + ', expected more than ' + j2);+ local s =+ if code.ctype == '%' then+ '%'+ else+ format_code(val, code, tmp.fw, tmp2.prec, j2);+ local s_padded =+ if code.cflags.left then+ pad_right(s, tmp.fw, ' ')+ else+ pad_left(s, tmp.fw, ' ');+ local j3 =+ if code.ctype == '%' then+ j2+ else+ j2 + 1;+ format_codes_arr(codes, arr, i + 1, j3, v + s_padded) tailstrict;++ // Render a parsed format string with an object of values.+ local format_codes_obj(codes, obj, i, v) =+ if i >= std.length(codes) then+ v+ else+ local code = codes[i];+ if std.type(code) == 'string' then+ format_codes_obj(codes, obj, i + 1, v + code) tailstrict+ else+ local f =+ if code.mkey == null then+ error 'Mapping keys required.'+ else+ code.mkey;+ local fw =+ if code.fw == '*' then+ error 'Cannot use * field width with object.'+ else+ code.fw;+ local prec =+ if code.prec == '*' then+ error 'Cannot use * precision with object.'+ else+ code.prec;+ local val =+ if std.objectHasAll(obj, f) then+ obj[f]+ else+ error 'No such field: ' + f;+ local s =+ if code.ctype == '%' then+ '%'+ else+ format_code(val, code, fw, prec, f);+ local s_padded =+ if code.cflags.left then+ pad_right(s, fw, ' ')+ else+ pad_left(s, fw, ' ');+ format_codes_obj(codes, obj, i + 1, v + s_padded) tailstrict;++ if std.isArray(vals) then+ format_codes_arr(codes, vals, 0, 0, '')+ else if std.isObject(vals) then+ format_codes_obj(codes, vals, 0, '')+ else+ format_codes_arr(codes, [vals], 0, 0, ''),++ foldr(func, arr, init)::+ local aux(func, arr, running, idx) =+ if idx < 0 then+ running+ else+ aux(func, arr, func(arr[idx], running), idx - 1) tailstrict;+ aux(func, arr, init, std.length(arr) - 1),++ foldl(func, arr, init)::+ local aux(func, arr, running, idx) =+ if idx >= std.length(arr) then+ running+ else+ aux(func, arr, func(running, arr[idx]), idx + 1) tailstrict;+ aux(func, arr, init, 0),+++ filterMap(filter_func, map_func, arr)::+ if !std.isFunction(filter_func) then+ error ('std.filterMap first param must be function, got ' + std.type(filter_func))+ else if !std.isFunction(map_func) then+ error ('std.filterMap second param must be function, got ' + std.type(map_func))+ else if !std.isArray(arr) then+ error ('std.filterMap third param must be array, got ' + std.type(arr))+ else+ std.map(map_func, std.filter(filter_func, arr)),++ assertEqual(a, b)::+ if a == b then+ true+ else+ error 'Assertion failed. ' + a + ' != ' + b,++ abs(n)::+ if !std.isNumber(n) then+ error 'std.abs expected number, got ' + std.type(n)+ else+ if n > 0 then n else -n,++ sign(n)::+ if !std.isNumber(n) then+ error 'std.sign expected number, got ' + std.type(n)+ else+ if n > 0 then+ 1+ else if n < 0 then+ -1+ else 0,++ max(a, b)::+ if !std.isNumber(a) then+ error 'std.max first param expected number, got ' + std.type(a)+ else if !std.isNumber(b) then+ error 'std.max second param expected number, got ' + std.type(b)+ else+ if a > b then a else b,++ min(a, b)::+ if !std.isNumber(a) then+ error 'std.min first param expected number, got ' + std.type(a)+ else if !std.isNumber(b) then+ error 'std.min second param expected number, got ' + std.type(b)+ else+ if a < b then a else b,++ clamp(x, minVal, maxVal)::+ if x < minVal then minVal+ else if x > maxVal then maxVal+ else x,++ flattenArrays(arrs)::+ std.foldl(function(a, b) a + b, arrs, []),++ manifestIni(ini)::+ local body_lines(body) =+ std.join([], [+ local value_or_values = body[k];+ if std.isArray(value_or_values) then+ ['%s = %s' % [k, value] for value in value_or_values]+ else+ ['%s = %s' % [k, value_or_values]]++ for k in std.objectFields(body)+ ]);++ local section_lines(sname, sbody) = ['[%s]' % [sname]] + body_lines(sbody),+ main_body = if std.objectHas(ini, 'main') then body_lines(ini.main) else [],+ all_sections = [+ section_lines(k, ini.sections[k])+ for k in std.objectFields(ini.sections)+ ];+ std.join('\n', main_body + std.flattenArrays(all_sections) + ['']),++ manifestToml(value):: std.manifestTomlEx(value, ' '),++ manifestTomlEx(value, indent)::+ local+ escapeStringToml = std.escapeStringJson,+ escapeKeyToml(key) =+ local bare_allowed = std.set(std.stringChars("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"));+ if std.setUnion(std.set(std.stringChars(key)), bare_allowed) == bare_allowed then key else escapeStringToml(key),+ isTableArray(v) = std.isArray(v) && std.length(v) > 0 && std.foldl(function(a, b) a && std.isObject(b), v, true),+ isSection(v) = std.isObject(v) || isTableArray(v),+ renderValue(v, indexedPath, inline, cindent) =+ if v == true then+ 'true'+ else if v == false then+ 'false'+ else if v == null then+ error 'Tried to manifest "null" at ' + indexedPath+ else if std.isNumber(v) then+ '' + v+ else if std.isString(v) then+ escapeStringToml(v)+ else if std.isFunction(v) then+ error 'Tried to manifest function at ' + indexedPath+ else if std.isArray(v) then+ if std.length(v) == 0 then+ '[]'+ else+ local range = std.range(0, std.length(v) - 1);+ local new_indent = if inline then '' else cindent + indent;+ local separator = if inline then ' ' else '\n';+ local lines = ['[' + separator]+ + std.join([',' + separator],+ [+ [new_indent + renderValue(v[i], indexedPath + [i], true, '')]+ for i in range+ ])+ + [separator + (if inline then '' else cindent) + ']'];+ std.join('', lines)+ else if std.isObject(v) then+ local lines = ['{ ']+ + std.join([', '],+ [+ [escapeKeyToml(k) + ' = ' + renderValue(v[k], indexedPath + [k], true, '')]+ for k in std.objectFields(v)+ ])+ + [' }'];+ std.join('', lines),+ renderTableInternal(v, path, indexedPath, cindent) =+ local kvp = std.flattenArrays([+ [cindent + escapeKeyToml(k) + ' = ' + renderValue(v[k], indexedPath + [k], false, cindent)]+ for k in std.objectFields(v)+ if !isSection(v[k])+ ]);+ local sections = [std.join('\n', kvp)] + [+ (if std.isObject(v[k]) then+ renderTable(v[k], path + [k], indexedPath + [k], cindent)+ else+ renderTableArray(v[k], path + [k], indexedPath + [k], cindent)+ )+ for k in std.objectFields(v)+ if isSection(v[k])+ ];+ std.join('\n\n', sections),+ renderTable(v, path, indexedPath, cindent) =+ cindent + '[' + std.join('.', std.map(escapeKeyToml, path)) + ']'+ + (if v == {} then '' else '\n')+ + renderTableInternal(v, path, indexedPath, cindent + indent),+ renderTableArray(v, path, indexedPath, cindent) =+ local range = std.range(0, std.length(v) - 1);+ local sections = [+ (cindent + '[[' + std.join('.', std.map(escapeKeyToml, path)) + ']]'+ + (if v[i] == {} then '' else '\n')+ + renderTableInternal(v[i], path, indexedPath + [i], cindent + indent))+ for i in range+ ];+ std.join('\n\n', sections);+ if std.isObject(value) then+ renderTableInternal(value, [], [], '')+ else+ error 'TOML body must be an object. Got ' + std.type(value),++ escapeStringJson(str_)::+ local str = std.toString(str_);+ local trans(ch) =+ if ch == '"' then+ '\\"'+ else if ch == '\\' then+ '\\\\'+ else if ch == '\b' then+ '\\b'+ else if ch == '\f' then+ '\\f'+ else if ch == '\n' then+ '\\n'+ else if ch == '\r' then+ '\\r'+ else if ch == '\t' then+ '\\t'+ else+ local cp = std.codepoint(ch);+ if cp < 32 || (cp >= 127 && cp <= 159) then+ '\\u%04x' % [cp]+ else+ ch;+ '"%s"' % std.join('', [trans(ch) for ch in std.stringChars(str)]),++ escapeStringPython(str)::+ std.escapeStringJson(str),++ escapeStringBash(str_)::+ local str = std.toString(str_);+ local trans(ch) =+ if ch == "'" then+ "'\"'\"'"+ else+ ch;+ "'%s'" % std.join('', [trans(ch) for ch in std.stringChars(str)]),++ escapeStringDollars(str_)::+ local str = std.toString(str_);+ local trans(ch) =+ if ch == '$' then+ '$$'+ else+ ch;+ std.foldl(function(a, b) a + trans(b), std.stringChars(str), ''),++ manifestJson(value):: std.manifestJsonEx(value, ' '),++ manifestJsonMinified(value):: std.manifestJsonEx(value, '', '', ':'),++ manifestJsonEx(value, indent, newline='\n', key_val_sep=': ')::+ local aux(v, path, cindent) =+ if v == true then+ 'true'+ else if v == false then+ 'false'+ else if v == null then+ 'null'+ else if std.isNumber(v) then+ '' + v+ else if std.isString(v) then+ std.escapeStringJson(v)+ else if std.isFunction(v) then+ error 'Tried to manifest function at ' + path+ else if std.isArray(v) then+ local range = std.range(0, std.length(v) - 1);+ local new_indent = cindent + indent;+ local lines = ['[' + newline]+ + std.join([',' + newline],+ [+ [new_indent + aux(v[i], path + [i], new_indent)]+ for i in range+ ])+ + [newline + cindent + ']'];+ std.join('', lines)+ else if std.isObject(v) then+ local lines = ['{' + newline]+ + std.join([',' + newline],+ [+ [cindent + indent + std.escapeStringJson(k) + key_val_sep+ + aux(v[k], path + [k], cindent + indent)]+ for k in std.objectFields(v)+ ])+ + [newline + cindent + '}'];+ std.join('', lines);+ aux(value, [], ''),++ manifestYamlDoc(value, indent_array_in_object=false)::+ local aux(v, path, cindent) =+ if v == true then+ 'true'+ else if v == false then+ 'false'+ else if v == null then+ 'null'+ else if std.isNumber(v) then+ '' + v+ else if std.isString(v) then+ local len = std.length(v);+ if len == 0 then+ '""'+ else if v[len - 1] == '\n' then+ local split = std.split(v, '\n');+ std.join('\n' + cindent + ' ', ['|'] + split[0:std.length(split) - 1])+ else+ std.escapeStringJson(v)+ else if std.isFunction(v) then+ error 'Tried to manifest function at ' + path+ else if std.isArray(v) then+ if std.length(v) == 0 then+ '[]'+ else+ local params(value) =+ if std.isArray(value) && std.length(value) > 0 then {+ // While we could avoid the new line, it yields YAML that is+ // hard to read, e.g.:+ // - - - 1+ // - 2+ // - - 3+ // - 4+ new_indent: cindent + ' ',+ space: '\n' + self.new_indent,+ } else if std.isObject(value) && std.length(value) > 0 then {+ new_indent: cindent + ' ',+ // In this case we can start on the same line as the - because the indentation+ // matches up then. The converse is not true, because fields are not always+ // 1 character long.+ space: ' ',+ } else {+ // In this case, new_indent is only used in the case of multi-line strings.+ new_indent: cindent,+ space: ' ',+ };+ local range = std.range(0, std.length(v) - 1);+ local parts = [+ '-' + param.space + aux(v[i], path + [i], param.new_indent)+ for i in range+ for param in [params(v[i])]+ ];+ std.join('\n' + cindent, parts)+ else if std.isObject(v) then+ if std.length(v) == 0 then+ '{}'+ else+ local params(value) =+ if std.isArray(value) && std.length(value) > 0 then {+ // Not indenting allows e.g.+ // ports:+ // - 80+ // instead of+ // ports:+ // - 80+ new_indent: if indent_array_in_object then cindent + ' ' else cindent,+ space: '\n' + self.new_indent,+ } else if std.isObject(value) && std.length(value) > 0 then {+ new_indent: cindent + ' ',+ space: '\n' + self.new_indent,+ } else {+ // In this case, new_indent is only used in the case of multi-line strings.+ new_indent: cindent,+ space: ' ',+ };+ local lines = [+ std.escapeStringJson(k) + ':' + param.space + aux(v[k], path + [k], param.new_indent)+ for k in std.objectFields(v)+ for param in [params(v[k])]+ ];+ std.join('\n' + cindent, lines);+ aux(value, [], ''),++ manifestYamlStream(value, indent_array_in_object=false, c_document_end=true)::+ if !std.isArray(value) then+ error 'manifestYamlStream only takes arrays, got ' + std.type(value)+ else+ '---\n' + std.join(+ '\n---\n', [std.manifestYamlDoc(e, indent_array_in_object) for e in value]+ ) + if c_document_end then '\n...\n' else '\n',+++ manifestPython(v)::+ if std.isObject(v) then+ local fields = [+ '%s: %s' % [std.escapeStringPython(k), std.manifestPython(v[k])]+ for k in std.objectFields(v)+ ];+ '{%s}' % [std.join(', ', fields)]+ else if std.isArray(v) then+ '[%s]' % [std.join(', ', [std.manifestPython(v2) for v2 in v])]+ else if std.isString(v) then+ '%s' % [std.escapeStringPython(v)]+ else if std.isFunction(v) then+ error 'cannot manifest function'+ else if std.isNumber(v) then+ std.toString(v)+ else if v == true then+ 'True'+ else if v == false then+ 'False'+ else if v == null then+ 'None',++ manifestPythonVars(conf)::+ local vars = ['%s = %s' % [k, std.manifestPython(conf[k])] for k in std.objectFields(conf)];+ std.join('\n', vars + ['']),++ manifestXmlJsonml(value)::+ if !std.isArray(value) then+ error 'Expected a JSONML value (an array), got %s' % std.type(value)+ else+ local aux(v) =+ if std.isString(v) then+ v+ else+ local tag = v[0];+ local has_attrs = std.length(v) > 1 && std.isObject(v[1]);+ local attrs = if has_attrs then v[1] else {};+ local children = if has_attrs then v[2:] else v[1:];+ local attrs_str =+ std.join('', [' %s="%s"' % [k, attrs[k]] for k in std.objectFields(attrs)]);+ std.deepJoin(['<', tag, attrs_str, '>', [aux(x) for x in children], '</', tag, '>']);++ aux(value),++ local base64_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',+ local base64_inv = { [base64_table[i]]: i for i in std.range(0, 63) },++ base64(input)::+ local bytes =+ if std.isString(input) then+ std.map(function(c) std.codepoint(c), input)+ else+ input;++ local aux(arr, i, r) =+ if i >= std.length(arr) then+ r+ else if i + 1 >= std.length(arr) then+ local str =+ // 6 MSB of i+ base64_table[(arr[i] & 252) >> 2] ++ // 2 LSB of i+ base64_table[(arr[i] & 3) << 4] ++ '==';+ aux(arr, i + 3, r + str) tailstrict+ else if i + 2 >= std.length(arr) then+ local str =+ // 6 MSB of i+ base64_table[(arr[i] & 252) >> 2] ++ // 2 LSB of i, 4 MSB of i+1+ base64_table[(arr[i] & 3) << 4 | (arr[i + 1] & 240) >> 4] ++ // 4 LSB of i+1+ base64_table[(arr[i + 1] & 15) << 2] ++ '=';+ aux(arr, i + 3, r + str) tailstrict+ else+ local str =+ // 6 MSB of i+ base64_table[(arr[i] & 252) >> 2] ++ // 2 LSB of i, 4 MSB of i+1+ base64_table[(arr[i] & 3) << 4 | (arr[i + 1] & 240) >> 4] ++ // 4 LSB of i+1, 2 MSB of i+2+ base64_table[(arr[i + 1] & 15) << 2 | (arr[i + 2] & 192) >> 6] ++ // 6 LSB of i+2+ base64_table[(arr[i + 2] & 63)];+ aux(arr, i + 3, r + str) tailstrict;++ local sanity = std.foldl(function(r, a) r && (a < 256), bytes, true);+ if !sanity then+ error 'Can only base64 encode strings / arrays of single bytes.'+ else+ aux(bytes, 0, ''),+++ base64DecodeBytes(str)::+ if std.length(str) % 4 != 0 then+ error 'Not a base64 encoded string "%s"' % str+ else+ local aux(str, i, r) =+ if i >= std.length(str) then+ r+ else+ // all 6 bits of i, 2 MSB of i+1+ local n1 = [base64_inv[str[i]] << 2 | (base64_inv[str[i + 1]] >> 4)];+ // 4 LSB of i+1, 4MSB of i+2+ local n2 =+ if str[i + 2] == '=' then []+ else [(base64_inv[str[i + 1]] & 15) << 4 | (base64_inv[str[i + 2]] >> 2)];+ // 2 LSB of i+2, all 6 bits of i+3+ local n3 =+ if str[i + 3] == '=' then []+ else [(base64_inv[str[i + 2]] & 3) << 6 | base64_inv[str[i + 3]]];+ aux(str, i + 4, r + n1 + n2 + n3) tailstrict;+ aux(str, 0, []),++ base64Decode(str)::+ local bytes = std.base64DecodeBytes(str);+ std.join('', std.map(function(b) std.char(b), bytes)),++ reverse(arr)::+ local l = std.length(arr);+ std.makeArray(l, function(i) arr[l - i - 1]),++ // Merge-sort for long arrays and naive quicksort for shorter ones+ sort(arr, keyF=id)::+ local quickSort(arr, keyF=id) =+ local l = std.length(arr);+ if std.length(arr) <= 1 then+ arr+ else+ local pos = 0;+ local pivot = keyF(arr[pos]);+ local rest = std.makeArray(l - 1, function(i) if i < pos then arr[i] else arr[i + 1]);+ local left = std.filter(function(x) keyF(x) < pivot, rest);+ local right = std.filter(function(x) keyF(x) >= pivot, rest);+ quickSort(left, keyF) + [arr[pos]] + quickSort(right, keyF);++ local merge(a, b) =+ local la = std.length(a), lb = std.length(b);+ local aux(i, j, prefix) =+ if i == la then+ prefix + b[j:]+ else if j == lb then+ prefix + a[i:]+ else+ if keyF(a[i]) <= keyF(b[j]) then+ aux(i + 1, j, prefix + [a[i]]) tailstrict+ else+ aux(i, j + 1, prefix + [b[j]]) tailstrict;+ aux(0, 0, []);++ local l = std.length(arr);+ if std.length(arr) <= 30 then+ quickSort(arr, keyF=keyF)+ else+ local mid = std.floor(l / 2);+ local left = arr[:mid], right = arr[mid:];+ merge(std.sort(left, keyF=keyF), std.sort(right, keyF=keyF)),++ uniq(arr, keyF=id)::+ local f(a, b) =+ if std.length(a) == 0 then+ [b]+ else if keyF(a[std.length(a) - 1]) == keyF(b) then+ a+ else+ a + [b];+ std.foldl(f, arr, []),++ set(arr, keyF=id)::+ std.uniq(std.sort(arr, keyF), keyF),++ setMember(x, arr, keyF=id)::+ // TODO(dcunnin): Binary chop for O(log n) complexity+ std.length(std.setInter([x], arr, keyF)) > 0,++ setUnion(a, b, keyF=id)::+ // NOTE: order matters, values in `a` win+ local aux(a, b, i, j, acc) =+ if i >= std.length(a) then+ acc + b[j:]+ else if j >= std.length(b) then+ acc + a[i:]+ else+ local ak = keyF(a[i]);+ local bk = keyF(b[j]);+ if ak == bk then+ aux(a, b, i + 1, j + 1, acc + [a[i]]) tailstrict+ else if ak < bk then+ aux(a, b, i + 1, j, acc + [a[i]]) tailstrict+ else+ aux(a, b, i, j + 1, acc + [b[j]]) tailstrict;+ aux(a, b, 0, 0, []),++ setInter(a, b, keyF=id)::+ local aux(a, b, i, j, acc) =+ if i >= std.length(a) || j >= std.length(b) then+ acc+ else+ if keyF(a[i]) == keyF(b[j]) then+ aux(a, b, i + 1, j + 1, acc + [a[i]]) tailstrict+ else if keyF(a[i]) < keyF(b[j]) then+ aux(a, b, i + 1, j, acc) tailstrict+ else+ aux(a, b, i, j + 1, acc) tailstrict;+ aux(a, b, 0, 0, []) tailstrict,++ setDiff(a, b, keyF=id)::+ local aux(a, b, i, j, acc) =+ if i >= std.length(a) then+ acc+ else if j >= std.length(b) then+ acc + a[i:]+ else+ if keyF(a[i]) == keyF(b[j]) then+ aux(a, b, i + 1, j + 1, acc) tailstrict+ else if keyF(a[i]) < keyF(b[j]) then+ aux(a, b, i + 1, j, acc + [a[i]]) tailstrict+ else+ aux(a, b, i, j + 1, acc) tailstrict;+ aux(a, b, 0, 0, []) tailstrict,++ mergePatch(target, patch)::+ if std.isObject(patch) then+ local target_object =+ if std.isObject(target) then target else {};++ local target_fields =+ if std.isObject(target_object) then std.objectFields(target_object) else [];++ local null_fields = [k for k in std.objectFields(patch) if patch[k] == null];+ local both_fields = std.setUnion(target_fields, std.objectFields(patch));++ {+ [k]:+ if !std.objectHas(patch, k) then+ target_object[k]+ else if !std.objectHas(target_object, k) then+ std.mergePatch(null, patch[k]) tailstrict+ else+ std.mergePatch(target_object[k], patch[k]) tailstrict+ for k in std.setDiff(both_fields, null_fields)+ }+ else+ patch,++ objectFields(o)::+ std.objectFieldsEx(o, false),++ objectFieldsAll(o)::+ std.objectFieldsEx(o, true),++ objectHas(o, f)::+ std.objectHasEx(o, f, false),++ objectHasAll(o, f)::+ std.objectHasEx(o, f, true),++ objectValues(o)::+ [o[k] for k in std.objectFields(o)],++ objectValuesAll(o)::+ [o[k] for k in std.objectFieldsAll(o)],++ equals(a, b)::+ local ta = std.type(a);+ local tb = std.type(b);+ if !std.primitiveEquals(ta, tb) then+ false+ else+ if std.primitiveEquals(ta, 'array') then+ local la = std.length(a);+ if !std.primitiveEquals(la, std.length(b)) then+ false+ else+ local aux(a, b, i) =+ if i >= la then+ true+ else if a[i] != b[i] then+ false+ else+ aux(a, b, i + 1) tailstrict;+ aux(a, b, 0)+ else if std.primitiveEquals(ta, 'object') then+ local fields = std.objectFields(a);+ local lfields = std.length(fields);+ if fields != std.objectFields(b) then+ false+ else+ local aux(a, b, i) =+ if i >= lfields then+ true+ else if local f = fields[i]; a[f] != b[f] then+ false+ else+ aux(a, b, i + 1) tailstrict;+ aux(a, b, 0)+ else+ std.primitiveEquals(a, b),+++ resolvePath(f, r)::+ local arr = std.split(f, '/');+ std.join('/', std.makeArray(std.length(arr) - 1, function(i) arr[i]) + [r]),++ prune(a)::+ local isContent(b) =+ if b == null then+ false+ else if std.isArray(b) then+ std.length(b) > 0+ else if std.isObject(b) then+ std.length(b) > 0+ else+ true;+ if std.isArray(a) then+ [std.prune(x) for x in a if isContent($.prune(x))]+ else if std.isObject(a) then {+ [x]: $.prune(a[x])+ for x in std.objectFields(a)+ if isContent(std.prune(a[x]))+ } else+ a,++ findSubstr(pat, str)::+ if !std.isString(pat) then+ error 'findSubstr first parameter should be a string, got ' + std.type(pat)+ else if !std.isString(str) then+ error 'findSubstr second parameter should be a string, got ' + std.type(str)+ else+ local pat_len = std.length(pat);+ local str_len = std.length(str);+ if pat_len == 0 || str_len == 0 || pat_len > str_len then+ []+ else+ std.filter(function(i) str[i:i + pat_len] == pat, std.range(0, str_len - pat_len)),++ find(value, arr)::+ if !std.isArray(arr) then+ error 'find second parameter should be an array, got ' + std.type(arr)+ else+ std.filter(function(i) arr[i] == value, std.range(0, std.length(arr) - 1)),++ // Three way comparison.+ // TODO(sbarzowski): consider exposing and documenting it properly+ __compare(v1, v2)::+ local t1 = std.type(v1), t2 = std.type(v2);+ if t1 != t2 then+ error "Comparison requires matching types. Got " + t1 + " and " + t2+ else if t1 == "array" then+ std.__compare_array(v1, v2)+ else if t1 == "function" || t1 == "object" || t1 == "bool" then+ error "Values of type " + t1 + " are not comparable."+ else if v1 < v2 then -1+ else if v1 > v2 then 1+ else 0,++ __compare_array(arr1, arr2)::+ local len1 = std.length(arr1), len2 = std.length(arr2);+ local minLen = std.min(len1, len2);+ local aux(i) =+ if i < minLen then+ local cmpRes = std.__compare(arr1[i], arr2[i]);+ if cmpRes != 0 then+ cmpRes+ else+ aux(i + 1) tailstrict+ else+ std.__compare(len1, len2);+ aux(0),++ __array_less(arr1, arr2):: std.__compare_array(arr1, arr2) == -1,+ __array_greater(arr1, arr2):: std.__compare_array(arr1, arr2) == 1,+ __array_less_or_equal(arr1, arr2):: std.__compare_array(arr1, arr2) <= 0,+ __array_greater_or_equal(arr1, arr2):: std.__compare_array(arr1, arr2) >= 0,++}
test/golden/Test.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-} -- | module Main where@@ -13,9 +14,16 @@ import Data.Text.Lazy import Data.Text.Lazy.Encoding (encodeUtf8) import Language.Jsonnet+import Language.Jsonnet.Annotate+import Language.Jsonnet.Desugar import Language.Jsonnet.Error+import Language.Jsonnet.Eval+import Language.Jsonnet.Eval (mergeWith)+import Language.Jsonnet.Eval.Monad import Language.Jsonnet.Pretty ()-import Language.Jsonnet.Std+import qualified Language.Jsonnet.Std.Lib as Lib+import Language.Jsonnet.Std.TH (mkStdlib)+import Language.Jsonnet.Value import System.FilePath (replaceExtension, takeBaseName) import Test.Tasty (TestTree, defaultMain, testGroup) import Test.Tasty.Golden (findByExtension, goldenVsString)@@ -37,16 +45,22 @@ goldenTests :: IO TestTree goldenTests = do jsonnetFiles <- findByExtension [".jsonnet"] "./test/golden"- runExceptT std >>= \case- Left err -> error (show $ pretty err)- Right stdlib -> do- return $- testGroup- "Jsonnet golden tests"- [ goldenVsString- (takeBaseName jsonnetFile) -- test name- goldenFile -- golden file path- (run $ Config jsonnetFile stdlib)- | jsonnetFile <- jsonnetFiles,- let goldenFile = replaceExtension jsonnetFile ".golden"- ]+ stdlib <- mkThunk std+ return $+ testGroup+ "Jsonnet golden tests"+ [ goldenVsString+ (takeBaseName jsonnetFile) -- test name+ goldenFile -- golden file path+ (run $ Config jsonnetFile stdlib)+ | jsonnetFile <- jsonnetFiles,+ let goldenFile = replaceExtension jsonnetFile ".golden"+ ]++-- the jsonnet stdlib is written in both jsonnet and Haskell, here we merge+-- the native (small, Haskell) with the interpreted (the splice mkStdlib)+std :: Eval Value+std = eval core >>= flip mergeObjects Lib.std+ where+ core = desugar (annMap (const ()) $mkStdlib)+ mergeObjects (VObj x) (VObj y) = pure $ VObj (x `mergeWith` y)