diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d624d2e..4dc2d20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,4 +24,7 @@ jobs: run: if [ -f scripts/test ]; then nix develop -c bash ./scripts/test; fi - name: Luacheck - run: nix develop -c luacheck --quiet --std lua51 --no-unused-args src/ + run: nix develop -c luacheck --quiet --std lua51 --no-unused-args --max-line-length 130 src/ + + - name: Format check + run: nix fmt && git diff --exit-code diff --git a/.gitignore b/.gitignore index db67e9a..e070528 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /.* !/.gitignore !/.github/ +!/.tidyrc.json +!/.lua-format /output/ diff --git a/.lua-format b/.lua-format index c5bb8bf..2945014 100644 --- a/.lua-format +++ b/.lua-format @@ -1,32 +1,10 @@ -align_args: true -align_parameter: true -align_table_field: true -break_after_functioncall_lp: false -break_after_functiondef_lp: false -break_after_operator: true -break_after_table_lb: true -break_before_functioncall_rp: false -break_before_functiondef_rp: false -break_before_table_rb: true -chop_down_kv_table: true -chop_down_parameter: false -chop_down_table: false -column_limit: 120 -column_table_limit: 120 -continuation_indent_width: 2 -double_quote_to_single_quote: false -extra_sep_at_table_end: false +# LuaFormatter config for the hand-written FFI under src/. +# 2-space indent. Keep simple functions on one line; column_limit sits a few +# columns under luacheck's 130 limit because lua-format under-counts the leading +# indent and trailing comma, so this keeps every emitted line within 130. indent_width: 2 -keep_simple_control_block_one_line: true -keep_simple_function_one_line: true -line_breaks_after_function_body: 1 -line_separator: input -single_quote_to_double_quote: false -spaces_around_equals_in_field: true -spaces_before_call: 1 -spaces_inside_functioncall_parens: false -spaces_inside_functiondef_parens: false -spaces_inside_table_braces: false -tab_width: 2 -table_sep: "," use_tab: false +column_limit: 126 +continuation_indent_width: 2 +keep_simple_function_one_line: true +keep_simple_control_block_one_line: true diff --git a/.tidyrc.json b/.tidyrc.json new file mode 100644 index 0000000..8636af8 --- /dev/null +++ b/.tidyrc.json @@ -0,0 +1,10 @@ +{ + "importSort": "source", + "importWrap": "source", + "indent": 2, + "operatorsFile": null, + "ribbon": 1, + "typeArrowPlacement": "first", + "unicode": "source", + "width": 80 +} diff --git a/AGENTS.md b/AGENTS.md index dde531a..a312901 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,11 +4,21 @@ A PureScript→Lua FFI fork in the [`purescript-lua`](https://github.com/purescr ## Commands -All commands run inside the nix dev shell: - - Build: `nix develop -c ./scripts/build` - Test (only if the fork has `scripts/test`): `nix develop -c bash ./scripts/test` -- Lint: `nix develop -c luacheck --quiet --std lua51 --no-unused-args src/` +- Lint: `nix develop -c luacheck --quiet --std lua51 --no-unused-args --max-line-length 130 src/` +- Format: `nix fmt` (check: `nix fmt && git diff --exit-code`) + +## Formatting + +`nix fmt` runs treefmt (`treefmt.nix`): nixfmt for Nix, `dhall format`, purs-tidy +for `*.purs` (config in `.tidyrc.json`), and LuaFormatter for the `*.lua` FFI +(config in `.lua-format`). LuaFormatter is used over StyLua because it keeps the +parentheses pslua's foreign-file parser requires. The Lua line budget is 130 +columns, matching the `luacheck --max-line-length` above. The check is +content-based (`nix fmt && git diff --exit-code`) rather than `treefmt --ci`, +since the in-place formatters bump mtime even when content is unchanged, which +trips treefmt's `--fail-on-change`. CI and the pre-commit hook use it. ## Lua 5.1 target diff --git a/flake.lock b/flake.lock index 6b6c417..c47b792 100644 --- a/flake.lock +++ b/flake.lock @@ -740,7 +740,8 @@ "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "pslua": "pslua", - "purescript-overlay": "purescript-overlay" + "purescript-overlay": "purescript-overlay", + "treefmt-nix": "treefmt-nix" } }, "stackage": { @@ -803,6 +804,26 @@ "repo": "default", "type": "github" } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780220602, + "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "db947814a175b7ca6ded66e21383d938df01c227", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 5198865..4d86c4b 100644 --- a/flake.nix +++ b/flake.nix @@ -9,16 +9,33 @@ inputs.nixpkgs.follows = "nixpkgs"; }; pslua.url = "github:purescript-lua/purescript-lua"; + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, flake-utils, purescript-overlay, pslua }: - flake-utils.lib.eachDefaultSystem (system: + outputs = + { + self, + nixpkgs, + flake-utils, + purescript-overlay, + pslua, + treefmt-nix, + }: + flake-utils.lib.eachDefaultSystem ( + system: let pkgs = import nixpkgs { inherit system; overlays = [ purescript-overlay.overlays.default ]; }; - in { + treefmtEval = treefmt-nix.lib.evalModule pkgs ./treefmt.nix; + in + { + formatter = treefmtEval.config.build.wrapper; + checks.formatting = treefmtEval.config.build.check self; devShell = pkgs.mkShell { buildInputs = with pkgs; [ dhall @@ -31,8 +48,26 @@ spago-bin.spago-0_21_0 treefmt ]; + # Install a content-based pre-commit hook. It compares the working + # tree diff before and after `nix fmt`, so it only objects to changes + # the formatter itself introduces (not the developer's existing + # unstaged work) and is not fooled by formatters that only bump mtime. + # Rewritten each shell entry to stay in sync with this flake. + shellHook = '' + hook=.git/hooks/pre-commit + if [ -d .git ]; then + printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'before=$(git diff)' \ + 'nix fmt >/dev/null 2>&1 || exit 0' \ + '[ "$before" = "$(git diff)" ] || { echo "nix fmt changed files; re-stage them, then commit." >&2; exit 1; }' \ + > "$hook" + chmod +x "$hook" + fi + ''; }; - }); + } + ); # --- Flake Local Nix Configuration ---------------------------- nixConfig = { diff --git a/src/Data/Number.purs b/src/Data/Number.purs index 926b2e5..e15b97c 100644 --- a/src/Data/Number.purs +++ b/src/Data/Number.purs @@ -18,7 +18,8 @@ module Data.Number , max , min , pow - , remainder, (%) + , remainder + , (%) , round , sign , sin @@ -112,7 +113,9 @@ foreign import isFinite :: Number -> Boolean fromString :: String -> Maybe Number fromString str = runFn4 fromStringImpl str isFinite Just Nothing -foreign import fromStringImpl :: Fn4 String (Number -> Boolean) (forall a. a -> Maybe a) (forall a. Maybe a) (Maybe Number) +foreign import fromStringImpl + :: Fn4 String (Number -> Boolean) (forall a. a -> Maybe a) (forall a. Maybe a) + (Maybe Number) -- | Returns the absolute value of the argument. -- | ```purs diff --git a/src/Data/Number/Approximate.purs b/src/Data/Number/Approximate.purs index b8afcf5..0b747dc 100644 --- a/src/Data/Number/Approximate.purs +++ b/src/Data/Number/Approximate.purs @@ -42,9 +42,9 @@ newtype Fraction = Fraction Number -- | true -- | ``` eqRelative :: Fraction -> Number -> Number -> Boolean -eqRelative (Fraction frac) 0.0 y = abs y <= frac -eqRelative (Fraction frac) x 0.0 = abs x <= frac -eqRelative (Fraction frac) x y = abs (x - y) <= frac * abs (x + y) / 2.0 +eqRelative (Fraction frac) 0.0 y = abs y <= frac +eqRelative (Fraction frac) x 0.0 = abs x <= frac +eqRelative (Fraction frac) x y = abs (x - y) <= frac * abs (x + y) / 2.0 -- | Test if two numbers are approximately equal, up to a relative difference -- | of one part in a million: @@ -63,8 +63,8 @@ eqRelative (Fraction frac) x y = abs (x - y) <= frac * abs (x + y) / 2.0 eqApproximate :: Number -> Number -> Boolean eqApproximate = eqRelative onePPM where - onePPM :: Fraction - onePPM = Fraction 1.0e-6 + onePPM :: Fraction + onePPM = Fraction 1.0e-6 infix 4 eqApproximate as ~= infix 4 eqApproximate as ≅ diff --git a/src/Data/Number/Format.purs b/src/Data/Number/Format.purs index 6cf4080..fa6d406 100644 --- a/src/Data/Number/Format.purs +++ b/src/Data/Number/Format.purs @@ -30,8 +30,8 @@ module Data.Number.Format import Prelude -foreign import toPrecisionNative :: Int -> Number -> String -foreign import toFixedNative :: Int -> Number -> String +foreign import toPrecisionNative :: Int -> Number -> String +foreign import toFixedNative :: Int -> Number -> String foreign import toExponentialNative :: Int -> Number -> String -- | The `Format` data type specifies how a number will be formatted. @@ -57,8 +57,8 @@ exponential = Exponential <<< clamp 0 20 -- | Convert a number to a string with a given format. toStringWith :: Format -> Number -> String -toStringWith (Precision p) = toPrecisionNative p -toStringWith (Fixed p) = toFixedNative p +toStringWith (Precision p) = toPrecisionNative p +toStringWith (Fixed p) = toFixedNative p toStringWith (Exponential p) = toExponentialNative p -- | Convert a number to a string via JavaScript's toString method. diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 98e466d..0f5c874 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -60,20 +60,27 @@ infix 1 eqAbsolute' as =~= numbersTestCode :: Effect Unit numbersTestCode = do - log "Data.Number.fromString" log "\tvalid number string" - assertTrue' "integer strings are coerced" $ - fromMaybe false $ map (_ == 123.0) $ fromString "123" - - assertTrue' "decimals are coerced" $ - fromMaybe false $ map (_ == 12.34) $ fromString "12.34" - - assertTrue' "exponents are coerced" $ - fromMaybe false $ map (_ == 1e4) $ fromString "1e4" - - assertTrue' "decimals exponents are coerced" $ - fromMaybe false $ map (_ == 1.2e4) $ fromString "1.2e4" + assertTrue' "integer strings are coerced" + $ fromMaybe false + $ map (_ == 123.0) + $ fromString "123" + + assertTrue' "decimals are coerced" + $ fromMaybe false + $ map (_ == 12.34) + $ fromString "12.34" + + assertTrue' "exponents are coerced" + $ fromMaybe false + $ map (_ == 1e4) + $ fromString "1e4" + + assertTrue' "decimals exponents are coerced" + $ fromMaybe false + $ map (_ == 1.2e4) + $ fromString "1.2e4" log "\tinvalid number string" assertTrue' "invalid strings are not coerced" $ @@ -215,7 +222,7 @@ numbersTestCode = do assertFalse' "should return false for differences larger than 10%" $ 0.000000000001 ~= 0.00000000000111 - -- assertFalse + -- assertFalse assertFalse' "should return false for differences larger than 10%" $ 0.000000000001 ~= 0.0000000000009 @@ -259,7 +266,6 @@ numbersTestCode = do assertTrue' "should work for 'fractions' larger than one" $ eqRelative (Fraction 3.0) 10.0 29.5 - log "Data.Number.Approximate.eqApproximate" log "\t0.1 + 0.2 ≅ 0.3" assertTrue' "0.1 + 0.2 should be approximately equal to 0.3" $ @@ -268,7 +274,6 @@ numbersTestCode = do assertTrue' "0.1 + 0.200001 should not be approximately equal to 0.3" $ 0.1 + 0.200001 ≇ 0.3 - log "Data.Number.Approximate.eqAbsolute" log "\teqAbsolute" assertTrue' "should succeed for differences smaller than the tolerance" $ diff --git a/test/regression/format.lua b/test/regression/format.lua index 0fb2d6d..8e93aa4 100644 --- a/test/regression/format.lua +++ b/test/regression/format.lua @@ -3,6 +3,6 @@ local M = assert(dofile("src/Data/Number/Format.lua")) assert(type(M.toPrecisionNative(6)(1234.56789)) == "string", - "toPrecisionNative must format its bound argument, not error on a nil global") + "toPrecisionNative must format its bound argument, not error on a nil global") print("OK Format.lua uses the bound argument n") diff --git a/treefmt.nix b/treefmt.nix new file mode 100644 index 0000000..0f57573 --- /dev/null +++ b/treefmt.nix @@ -0,0 +1,43 @@ +{ pkgs, ... }: +{ + projectRootFile = "flake.nix"; + + # Nix — RFC 166 formatter. + programs.nixfmt.enable = true; + + # Dhall — spago.dhall / packages.dhall layout. + programs.dhall.enable = true; + + # PureScript — purs-tidy is not a first-class treefmt program, so wire it via + # the generic mechanism. It picks up `.tidyrc.json` from the project root. + settings.formatter.purs-tidy = { + command = "${pkgs.purs-tidy}/bin/purs-tidy"; + options = [ "format-in-place" ]; + includes = [ "*.purs" ]; + }; + + # Lua FFI — LuaFormatter keeps the parentheses pslua's foreign-file parser + # requires (unlike StyLua, which strips them). Config in `.lua-format`. + settings.formatter.lua-format = { + command = "${pkgs.luaformatter}/bin/lua-format"; + options = [ + "-i" + "-c" + ".lua-format" + ]; + includes = [ "*.lua" ]; + }; + + # Never format generated output or vendored trees. + settings.global.excludes = [ + "dist/*" + "output/*" + ".spago/*" + "node_modules/*" + "*.lock" + "flake.lock" + "spago.lock" + ".tidyrc.json" + ".lua-format" + ]; +}