Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"Monoid",
"Nanotime",
"NEOHASKELL",
"nhcli",
"nhcore",
"nixfmt",
"nixpkgs",
Expand Down
4 changes: 2 additions & 2 deletions cli/nhcli.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 3.4
name: nhcli
version: 0.9.0
version: 0.9.1
synopsis: Command Line Tool for NeoHaskell
description: NeoHaskell is a dialect of Haskell that is focused on newcomer-friendliness and productivity.
homepage: https://neohaskell.org
Expand All @@ -11,7 +11,7 @@ maintainer: nhlib@nickseagull.dev
copyright:
category: Development
build-type: Simple
extra-source-files: ../nix/haskellnix.nix, ../nix/haskell-project.nix
extra-source-files: ./nix/haskell-project.nix

common common_cfg
ghc-options: -Wall
Expand Down
40 changes: 28 additions & 12 deletions cli/nix/haskell-project.nix
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
{ pkgs }:

# Generic function to create a Haskell project template
{ projectName, src }:
{ packages, mainPackageName, executableName }:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Verification agent

🧩 Analysis chain

Parameterize the compiler to avoid silent breakage when haskell.nix moves; verify ghc910 availability

Hard-coding compiler-nix-name = "ghc910" couples you to a specific compiler and may break as the pinned haskell.nix evolves. Make it configurable and default sensibly; also confirm that ghc910 is supported by your pinned commit.

Apply this diff to parameterize:

-{ packages, mainPackageName, executableName }:
+{ packages, mainPackageName, executableName, compilerNixName ? "ghc910" }:

And this one to use it:

-            compiler-nix-name = "ghc910";
+            compiler-nix-name = compilerNixName;

To confirm the compiler alias is valid for your pin, run a quick web check:

Also applies to: 19-20


🌐 Web query:

For haskell.nix commit 4d493449406ec91db804511a6d15b6f076ba40e7, what values are accepted for `compiler-nix-name`, and is `ghc910` supported?

πŸ’‘ Result:

For haskell.nix commit 4d493449406ec91db804511a6d15b6f076ba40e7, the accepted values for compiler-nix-name are compiler names like ghc96, ghc967, ghc98, ghc984, ghc910, ghc9102, ghc912, and ghc9122, each corresponding to specific GHC versions[2][8]. ghc910 is explicitly supported as one of the accepted values for GHC 9.10.x[2].

Key details:

  • Values for compiler-nix-name:
    • Examples: ghc96, ghc967 (GHC 9.6.x), ghc98, ghc984 (GHC 9.8.x), ghc910, ghc9102 (GHC 9.10.x), ghc912, ghc9122 (GHC 9.12.x)[2][8].
    • You should consult the project's source or overlays/bootstrap.nix for the complete, up-to-date list, as additional minor patch versions may also be accepted (e.g., ghc9102)[2].
  • Supported status:
    • The commit in question supports ghc910 as a valid value, and it is listed as tested in CI for nixpkgs-unstable[2].
  • How to use:
    • In configuration functions such as callCabalProjectToNix, set compiler-nix-name = "ghc910" to use GHC 9.10.x[8].
  • If a name does not match a defined compiler, you will see a Nix errorβ€”use the exact value from the documentation or table[2][6].
GHC Version compiler-nix-name example Supported/Tested?
9.6.x ghc96, ghc967 Yes
9.8.x ghc98, ghc984 Yes
9.10.x ghc910, ghc9102 Yes
9.12.x ghc912, ghc9122 Yes

Additional notes:

  • While ghc910 is supported and cached, experimental/minor patch versions may work if present in overlays/bootstrap.nix, but build times may be longer if not cached[2].
  • If you require an older or newer compiler not listed, advanced users can add new GHC versions following haskell.nix contributing instructions[2].

In summary, for haskell.nix at 4d493449406ec91db804511a6d15b6f076ba40e7, use ghc910 to select GHC 9.10.xβ€”the value is supported and actively tested[2][8].

Citations:


Parameterize compiler-nix-name with default "ghc910"

We’ve confirmed that ghc910 is natively supported in haskell.nix at commit 4d493449406ec91db804511a6d15b6f076ba40e7 (GHC 9.10.x). To prevent silent breakage when upstream evolves, refactor as follows:

β€’ In cli/nix/haskell-project.nix, change the function signature (around line 3):

-{ packages, mainPackageName, executableName }:
+{ packages, mainPackageName, executableName, compilerNixName ? "ghc910" }:

β€’ Replace the hard-coded compiler name (around lines 19–20):

-            compiler-nix-name = "ghc910";
+            compiler-nix-name = compilerNixName;

This makes the compiler choice configurable (defaulting to a verified, supported value) and guards against future compatibility issues.

πŸ€– Prompt for AI Agents
In cli/nix/haskell-project.nix around line 3, add a new optional parameter to
the function signature: compiler-nix-name ? "ghc910", and in the body around
lines 19–20 replace the hard-coded "ghc910" literal with the compiler-nix-name
parameter so the compiler can be configured while defaulting to the verified
ghc910 value.

let
haskellNix = import (pkgs.fetchFromGitHub {
owner = "input-output-hk";
repo = "haskell.nix";
rev = "4d493449406ec91db804511a6d15b6f076ba40e7";
sha256 = "sha256-CHNMDgFfpTV5WkVhmMLf5d5qaLUjgeoziVgmgnhPGrI=";
}) { inherit pkgs; };
# Apply haskell.nix overlay
}) { };
pkgsWithOverlay = import haskellNix.sources.nixpkgs-unstable {
overlays = [
haskellNix.overlay
(final: _prev: {
"${projectName}Project" = final.haskell-nix.cabalProject {
inherit src;
compiler-nix-name = "ghc910";
};
})
(final: _prev:
builtins.listToAttrs (builtins.map (packageName: {
name = "${packageName}";
value = final.haskell-nix.cabalProject {
src = packages.${packageName};
compiler-nix-name = "ghc910";
};
}) (builtins.attrNames packages)))
Comment on lines +14 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Quote hyphenated attribute 'haskell-nix' to avoid a parse error

Attribute names with hyphens must be quoted in Nix. As written, final.haskell-nix.cabalProject will not parse correctly.

Apply this diff:

-          value = final.haskell-nix.cabalProject {
+          value = final."haskell-nix".cabalProject {
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(final: _prev:
builtins.listToAttrs (builtins.map (packageName: {
name = "${packageName}";
value = final.haskell-nix.cabalProject {
src = packages.${packageName};
compiler-nix-name = "ghc910";
};
}) (builtins.attrNames packages)))
(final: _prev:
builtins.listToAttrs (builtins.map (packageName: {
name = "${packageName}";
value = final."haskell-nix".cabalProject {
src = packages.${packageName};
compiler-nix-name = "ghc910";
};
}) (builtins.attrNames packages)))
πŸ€– Prompt for AI Agents
In cli/nix/haskell-project.nix around lines 14 to 21, attribute names containing
hyphens (like haskell-nix) must be quoted to parse correctly; replace usages
such as final.haskell-nix.cabalProject with final."haskell-nix".cabalProject
(and any other occurrences of haskell-nix in attribute access) so the attribute
name is quoted, leaving the rest of the expression unchanged.

];
Comment on lines 11 to 22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Name collisions in the overlay are possible; consider a namespaced project set.

Injecting each package at the top level (pkgsWithOverlay.nhcli, pkgsWithOverlay.nhcore) risks collisions with existing attributes. Consider nesting under a single attribute (e.g., final.neohaskellPackages) and selecting from there.

Apply:

-      (final: _prev:
-        builtins.listToAttrs (builtins.map (packageName: {
-          name = "${packageName}";
-          value = final."haskell-nix".cabalProject {
-            src = packages.${packageName};
-            compiler-nix-name = compilerNixName;
-          };
-        }) (builtins.attrNames packages)))
+      (final: _prev:
+        {
+          neohaskellPackages =
+            builtins.listToAttrs (builtins.map (packageName: {
+              name = packageName;
+              value = final."haskell-nix".cabalProject {
+                src = packages.${packageName};
+                compiler-nix-name = compilerNixName;
+              };
+            }) (builtins.attrNames packages));
+        })

And later:

-  pkg = builtins.getAttr mainPackageName pkgsWithOverlay;
+  pkg = builtins.getAttr mainPackageName pkgsWithOverlay.neohaskellPackages;
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pkgsWithOverlay = import haskellNix.sources.nixpkgs-unstable {
overlays = [
haskellNix.overlay
(final: _prev: {
"${projectName}Project" = final.haskell-nix.cabalProject {
inherit src;
compiler-nix-name = "ghc910";
};
})
(final: _prev:
builtins.listToAttrs (builtins.map (packageName: {
name = "${packageName}";
value = final.haskell-nix.cabalProject {
src = packages.${packageName};
compiler-nix-name = "ghc910";
};
}) (builtins.attrNames packages)))
];
pkgsWithOverlay = import haskellNix.sources.nixpkgs-unstable {
overlays = [
haskellNix.overlay
# Namespace all generated projects under `neohaskellPackages`
(final: _prev:
{
neohaskellPackages =
builtins.listToAttrs (builtins.map (packageName: {
name = packageName;
value = final."haskell-nix".cabalProject {
src = packages.${packageName};
compiler-nix-name = compilerNixName;
};
}) (builtins.attrNames packages));
})
];
};
# Select from the namespaced set instead of the top level
pkg = builtins.getAttr mainPackageName pkgsWithOverlay.neohaskellPackages;
πŸ€– Prompt for AI Agents
In cli/nix/haskell-project.nix around lines 11–22, the current overlay injects
each package directly at the top level which can cause name collisions; instead
create a namespaced attribute (e.g., final.neohaskellPackages) that contains the
generated package set. Modify the overlay so it adds a single attribute like
neohaskellPackages = builtins.listToAttrs(...mapping...) rather than returning
the mapped attrs at the top level, keep the same mapping logic and names, and
update any code that referenced pkgsWithOverlay.<pkg> to select from
pkgsWithOverlay.neohaskellPackages.<pkg>.

inherit (haskellNix) config;
};
Comment on lines 11 to 24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Lock imported nixpkgs system to match caller to prevent accidental cross-system evaluation.

Pass system = pkgs.system when importing nixpkgs-unstable to ensure consistency with the outer pkgs.

-  pkgsWithOverlay = import haskellNix.sources.nixpkgs-unstable {
+  pkgsWithOverlay = import haskellNix.sources.nixpkgs-unstable {
+    system = pkgs.system;
     overlays = [
       haskellNix.overlay
       (final: _prev:
         builtins.listToAttrs (builtins.map (packageName: {
           name = "${packageName}";
           value = final."haskell-nix".cabalProject {
             src = packages.${packageName};
             compiler-nix-name = compilerNixName;
           };
         }) (builtins.attrNames packages)))
     ];
     inherit (haskellNix) config;
   };
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pkgsWithOverlay = import haskellNix.sources.nixpkgs-unstable {
overlays = [
haskellNix.overlay
(final: _prev: {
"${projectName}Project" = final.haskell-nix.cabalProject {
inherit src;
compiler-nix-name = "ghc910";
};
})
(final: _prev:
builtins.listToAttrs (builtins.map (packageName: {
name = "${packageName}";
value = final.haskell-nix.cabalProject {
src = packages.${packageName};
compiler-nix-name = "ghc910";
};
}) (builtins.attrNames packages)))
];
inherit (haskellNix) config;
};
pkgsWithOverlay = import haskellNix.sources.nixpkgs-unstable {
system = pkgs.system;
overlays = [
haskellNix.overlay
(final: _prev:
builtins.listToAttrs (builtins.map (packageName: {
name = "${packageName}";
value = final.haskell-nix.cabalProject {
src = packages.${packageName};
compiler-nix-name = "ghc910";
};
}) (builtins.attrNames packages)))
];
inherit (haskellNix) config;
};
πŸ€– Prompt for AI Agents
In cli/nix/haskell-project.nix around lines 11 to 24 the import of
haskellNix.sources.nixpkgs-unstable does not specify a system, which can cause
cross-system evaluation; update the import call to pass system = pkgs.system
(i.e. import haskellNix.sources.nixpkgs-unstable { system = pkgs.system;
overlays = [ ... ]; inherit (haskellNix) config; }) so the imported nixpkgs is
locked to the outer pkgs system.


flake = pkgsWithOverlay."${projectName}Project".flake { };
pkg = pkgsWithOverlay."${mainPackageName}";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Prefer builtins.getAttr for dynamic attribute selection

Using a string-literal selector here works, but builtins.getAttr is idiomatic and avoids edge cases with interpolation in attr paths.

Apply this diff:

-  pkg = pkgsWithOverlay."${mainPackageName}";
+  pkg = builtins.getAttr mainPackageName pkgsWithOverlay;
πŸ€– Prompt for AI Agents
In cli/nix/haskell-project.nix around line 26, the attribute selection uses
string interpolation (pkg = pkgsWithOverlay."${mainPackageName}";) which is less
idiomatic and can have edge cases; replace it by using builtins.getAttr to
lookup the attribute dynamically: call builtins.getAttr with mainPackageName and
pkgsWithOverlay so pkg is assigned the attribute value safely and idiomatically.


in flake.packages."${projectName}:exe:${projectName}"
in {
shell = pkg.shellFor {
tools = {
cabal = { };
hlint = { };
haskell-language-server = { };
};
buildInputs = [
pkgs.nil
pkgs.nixfmt-classic
pkgs.nixpkgs-fmt
pkgs.pkg-config
pkgs.zlib
];
};
Comment on lines +29 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid mixing derivations from different nixpkgs instances in shell inputs

shellFor is created from pkgsWithOverlay (pinned via haskell.nix), but you feed it buildInputs from the outer pkgs (). Mixing store paths from different nixpkgs pins can cause subtle ABI issues and evaluation surprises. Source buildInputs from the same pkgsWithOverlay.

Apply this diff:

-  shell = pkg.shellFor {
+  shell = pkg.shellFor {
     tools = {
       cabal = { };
       hlint = { };
       haskell-language-server = { };
     };
-    buildInputs = [
-      pkgs.nil
-      pkgs.nixfmt-classic
-      pkgs.nixpkgs-fmt
-      pkgs.pkg-config
-      pkgs.zlib
-    ];
+    buildInputs = with pkgsWithOverlay; [
+      nil
+      nixfmt-classic
+      nixpkgs-fmt
+      pkg-config
+      zlib
+    ];
   };
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
shell = pkg.shellFor {
tools = {
cabal = { };
hlint = { };
haskell-language-server = { };
};
buildInputs = [
pkgs.nil
pkgs.nixfmt-classic
pkgs.nixpkgs-fmt
pkgs.pkg-config
pkgs.zlib
];
};
shell = pkg.shellFor {
tools = {
cabal = { };
hlint = { };
haskell-language-server = { };
};
buildInputs = with pkgsWithOverlay; [
nil
nixfmt-classic
nixpkgs-fmt
pkg-config
zlib
];
};
πŸ€– Prompt for AI Agents
In cli/nix/haskell-project.nix around lines 29 to 42, the shellFor call mixes
buildInputs from the outer pkgs (unpinned nixpkgs) while shellFor itself comes
from pkgsWithOverlay (the pinned haskell.nix overlay); replace all references to
pkgs.* in buildInputs (and any tool references if needed) with the corresponding
pkgsWithOverlay.* items so all derivations come from the same pinned
pkgsWithOverlay instance, e.g., change pkgs.nil, pkgs.nixfmt-classic,
pkgs.nixpkgs-fmt, pkgs.pkg-config, pkgs.zlib to use pkgsWithOverlay.nil,
pkgsWithOverlay.nixfmt-classic, pkgsWithOverlay.nixpkgs-fmt,
pkgsWithOverlay.pkg-config, pkgsWithOverlay.zlib (or bind a local variable to
pkgsWithOverlay for clarity).

package = (pkg.flake { }).packages."${executableName}";
}
Comment on lines +44 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Fail early if executableName is wrong; add an assertion.

Guard against typos by asserting the package key exists in the flake outputs.

-  package = (pkg.flake { }).packages."${executableName}";
+  package =
+    let
+      flakeOut = pkg.flake { };
+      pkgsOut  = flakeOut.packages;
+    in
+      assert builtins.hasAttr executableName pkgsOut;
+      pkgsOut."${executableName}";
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
package = (pkg.flake { }).packages."${executableName}";
}
package =
let
flakeOut = pkg.flake { };
pkgsOut = flakeOut.packages;
in
assert builtins.hasAttr executableName pkgsOut;
pkgsOut."${executableName}";
}
πŸ€– Prompt for AI Agents
In cli/nix/haskell-project.nix around lines 44-45, the code directly indexes the
flake outputs with package = (pkg.flake { }).packages."${executableName}"; which
silently fails if executableName is misspelled; add a precondition using
builtins.hasAttr (or compute the packages attrset first) and assert that the key
exists before indexing, then use that validated attrset to select the package so
the build fails early with a clear assertion message when the package key is
missing.

2 changes: 1 addition & 1 deletion cli/src/Neo.hs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ run = do
Command.CommandOptions
{ name = "neo",
description = "NeoHaskell's console helper",
version = Just [Core.version|0.9.0|],
version = Just [Core.version|0.9.1|],
decoder = commandsParser
Comment on lines +52 to 53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Verification agent

🧩 Analysis chain

Version banner updated to 0.9.1 β€” approved.

Consistent with cabal version bumps in this PR.

Run this quick sweep to ensure no stale version references remain:


🏁 Script executed:

#!/bin/bash
# Find old version references and version banners
rg -nP -C2 '\[Core\.version\|\d+\.\d+\.\d+\|]' --type=haskell
rg -nP '0\.9\.0' -g '!**/dist/**' -g '!**/result/**'

Length of output: 570


Stale version references detectedβ€”update to 0.9.1 before merging.

The CLI banner in cli/src/Neo.hs is correctly bumped to 0.9.1, but the following outdated 0.9.0 pins remain:

β€’ website/package-lock.json:2318,9404
– "synckit": "^0.9.0" β†’ "^0.9.1"
β€’ website/package-lock.json:7630
– "@deno/kv": ">=0.9.0" β†’ ">=0.9.1"

Command the update of these entries and regenerate the lock file.

πŸ€– Prompt for AI Agents
In cli/src/Neo.hs around lines 52 to 53 the CLI version was bumped to 0.9.1 but
website/package-lock.json still pins older 0.9.0 versions; update
website/package-lock.json entries: change "synckit": "^0.9.0" to "^0.9.1" at
offsets ~2318 and ~9404 and change "@deno/kv": ">=0.9.0" to ">=0.9.1" at ~7630,
then regenerate the lock file (npm install / npm ci or your repo's lock regen
step) to ensure checksums and transitive entries are consistent before merging.

}
cmd <- Command.parseHandler parser
Expand Down
8 changes: 5 additions & 3 deletions cli/src/Neo/Build.hs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ handle config = do
File.writeText targetAppPath appMainFile
|> Task.mapError (\_ -> CustomError "Could not write app main file")

-- FIXME: Create another thread that renders the output of the build via streaming.
-- As right now there's no output at all
let buildExpression :: Text =
[fmt|{ pkgs ? import <nixpkgs> {} }:
( (#{nixFile}) { inherit pkgs; } ).package|]

completion <-
Subprocess.openInherit "nix-build" (Array.fromLinkedList ["-E", nixFile]) rootFolder Subprocess.InheritBOTH
Subprocess.openInherit "nix-build" (Array.fromLinkedList ["-E", buildExpression]) rootFolder Subprocess.InheritBOTH
Comment on lines +78 to +83

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Verification agent

🧩 Analysis chain

nix-build -E must evaluate to a derivation; don’t pass a lambda.

Like nix-shell, nix-build -E expects a derivation or set of derivations, not a top-level lambda. The current expression begins with a lambda and will not build.

Minimal fix using let/in:

-  let buildExpression :: Text =
-        [fmt|{ pkgs ? import <nixpkgs> {} }:
-  ( (#{nixFile}) { inherit pkgs; } ).package|]
+  let buildExpression :: Text =
+        [fmt|let pkgs = import <nixpkgs> {}; in
+  ( (#{nixFile}) { inherit pkgs; } ).package|]

Alternative (clearer target selection): evaluate the set and select -A package:

-  completion <-
-    Subprocess.openInherit "nix-build" (Array.fromLinkedList ["-E", buildExpression]) rootFolder Subprocess.InheritBOTH
+  let projectSetExpr :: Text =
+        [fmt|let pkgs = import <nixpkgs> {}; in
+  ( (#{nixFile}) { inherit pkgs; } )|]
+  completion <-
+    Subprocess.openInherit "nix-build" (Array.fromLinkedList ["-E", projectSetExpr, "-A", "package"]) rootFolder Subprocess.InheritBOTH

Also see my notes on pinning nixpkgs to avoid reliance on NIX_PATH.

Run to double-check:


🏁 Script executed:

#!/bin/bash
rg -n -C2 'nix-build' cli/src/Neo/Build.hs

Length of output: 260


Enforce derivation evaluation for nix-build -E

By my omniscient decree, nix-build -E must receive a derivation, not a naked lambda. The current buildExpression begins with a top-level lambda and will fail.

Required changes in cli/src/Neo/Build.hs:

β€’ Replace the lambda form of buildExpression (around lines 78–83) with a let/in that returns a derivation:

-  let buildExpression :: Text =
-        [fmt|{ pkgs ? import <nixpkgs> {} }:
-  ( (#{nixFile}) { inherit pkgs; } ).package|]
+  let buildExpression :: Text =
+        [fmt|let pkgs = import <nixpkgs> {}; in
+  ( (#{nixFile}) { inherit pkgs; } ).package|]

β€’ Alternatively, select the attribute from the set and pass it via -A package:

-  completion <-
-    Subprocess.openInherit "nix-build" (Array.fromLinkedList ["-E", buildExpression]) rootFolder Subprocess.InheritBOTH
+  let projectSetExpr :: Text =
+        [fmt|let pkgs = import <nixpkgs> {}; in
+  ( (#{nixFile}) { inherit pkgs; } )|]
+  completion <-
+    Subprocess.openInherit
+      "nix-build"
+      ( Array.fromLinkedList
+          [ "-E", projectSetExpr
+          , "-A", "package"
+          ]
+      )
+      rootFolder
+      Subprocess.InheritBOTH

β€’ Pin your <nixpkgs> import to a fixed commit or channel to avoid unpredictable builds via NIX_PATH.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let buildExpression :: Text =
[fmt|{ pkgs ? import <nixpkgs> {} }:
( (#{nixFile}) { inherit pkgs; } ).package|]
completion <-
Subprocess.openInherit "nix-build" (Array.fromLinkedList ["-E", nixFile]) rootFolder Subprocess.InheritBOTH
Subprocess.openInherit "nix-build" (Array.fromLinkedList ["-E", buildExpression]) rootFolder Subprocess.InheritBOTH
let buildExpression :: Text =
[fmt|let pkgs = import <nixpkgs> {}; in
( (#{nixFile}) { inherit pkgs; } ).package|]
let projectSetExpr :: Text =
[fmt|let pkgs = import <nixpkgs> {}; in
( (#{nixFile}) { inherit pkgs; } )|]
completion <-
Subprocess.openInherit
"nix-build"
( Array.fromLinkedList
[ "-E", projectSetExpr
, "-A", "package"
]
)
rootFolder
Subprocess.InheritBOTH
πŸ€– Prompt for AI Agents
In cli/src/Neo/Build.hs around lines 78 to 83 the buildExpression is a top-level
lambda which yields a naked function and fails with nix-build -E because it must
receive a derivation; replace the lambda form with a let/in that imports the
pinned nixpkgs and evaluates the expression to a derivation (for example bind
pkgs = import (builtins.fetchTarball "<fixed-commit-or-channel>") {}; let drv =
(#{nixFile}) { inherit pkgs; } in drv), or alternatively change invocation to
pass the set attribute via -A package and select the attribute from the imported
set, and ensure you pin <nixpkgs> to a fixed commit or channel instead of using
an unpinned <nixpkgs>.

if completion.exitCode != 0
then errorOut completion.stderr
else print completion.stdout
Expand Down
7 changes: 5 additions & 2 deletions cli/src/Neo/Build/Templates/Nix.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ let
hp = (#{haskellProject});

in (hp { inherit pkgs; }) {
projectName = "#{name}";
src = ./.;
packages = {
"#{name}" = ./.;
};
mainPackageName = "#{name}";
executableName = "#{name}:exe:#{name}";
Comment on lines +21 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Verification agent

🧩 Analysis chain

Confirm executableName matches the cabal executable stanza.

You’re selecting the build target by a fully-qualified executableName. Ensure this value matches the actual executable defined in the cabal file; otherwise nix-build will fail to resolve the attribute.

If this template is used for arbitrary projects, consider deriving executableName from the generated Cabal template to avoid drift.

[suggest_nitpick]
Use this script to list cabal package and executable names:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Cabal packages and executables detected:"
fd -t f --extension cabal -a | while read -r f; do
  echo "--- $f"
  rg -nP '^\s*name\s*:\s*(\S+)' "$f" || true
  rg -nP '^\s*executable\s+(\S+)' -n "$f" || true
done

Length of output: 719


Fix Nix executableName to match Cabal executable

The nhcli.cabal file declares the executable stanza as β€œneo”, but your Nix template uses:

executableName = "#{name}:exe:#{name}";

which expands to nhcli:exe:nhcli and will fail to resolve.

Please update in cli/src/Neo/Build/Templates/Nix.hs (lines 21-25):

  • Replace
    executableName = "#{name}:exe:#{name}";
    with
    executableName = "#{name}:exe:neo";

To prevent this slipping out of sync in future, consider deriving executableName automatically from the .cabal manifest rather than hard-coding it.

πŸ€– Prompt for AI Agents
In cli/src/Neo/Build/Templates/Nix.hs around lines 21 to 25, the Nix template
sets executableName incorrectly to "#{name}:exe:#{name}" which expands to
nhcli:exe:nhcli and won't match the cabal executable named "neo"; change the
executableName value to "#{name}:exe:neo" so it resolves to nhcli:exe:neo and
matches the cabal stanza; optionally consider deriving the executable name from
the .cabal manifest in the future to avoid hard-coding.

}

|]
5 changes: 3 additions & 2 deletions cli/src/Neo/Shell.hs
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ handle config = do
File.writeText targetAppPath appMainFile
|> Task.mapError (\_ -> CustomError "Could not write app main file")

let shellExpression =
let shellExpression :: Text =
[fmt|{ pkgs ? import <nixpkgs> {} }:
( (#{nixFile}) { inherit pkgs; } ).env|]
( (#{nixFile}) { inherit pkgs; } ).shell|]

Comment on lines +78 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Verification agent

🧩 Analysis chain

Stop returning a lambda to nix-shell; emit a derivation (use let/in).

nix-shell -E expects an expression that evaluates to a derivation or a shell env, not a top-level lambda. The current expression starts with a lambda and will not be evaluated as intended.

Apply this fix to bind pkgs via let/in and return the shell directly:

-  let shellExpression :: Text =
-        [fmt|{ pkgs ? import <nixpkgs> {} }:
-  ( (#{nixFile}) { inherit pkgs; } ).shell|]
+  let shellExpression :: Text =
+        [fmt|let pkgs = import <nixpkgs> {}; in
+  ( (#{nixFile}) { inherit pkgs; } ).shell|]

Follow-up: the direct import of assumes NIX_PATH is set. Either pin nixpkgs (preferred) or ensure CI sets NIX_PATH/-I. See my comments on default.nix/common.nix for a robust pin.

Run this to confirm nix-shell evaluates to a derivation and not a lambda:


🏁 Script executed:

#!/bin/bash
# Show what the expression evaluates to (should not be a lambda)
rg -n -C2 'shellExpression' cli/src/Neo/Shell.hs

Length of output: 477


Stop returning a top‐level lambda to nix-shell; emit a derivation

nix-shell -E requires an expression that evaluates directly to a derivation or shell env, not a lambda. In cli/src/Neo/Shell.hs (lines 78–81), replace:

-  let shellExpression :: Text =
-        [fmt|{ pkgs ? import <nixpkgs> {} }:
-  ( (#{nixFile}) { inherit pkgs; } ).shell|]
+  let shellExpression :: Text =
+        [fmt|let pkgs = import <nixpkgs> {}; in
+  ( (#{nixFile}) { inherit pkgs; } ).shell|]

Additionally, avoid relying on an externally set NIX_PATH. Either pin nixpkgs to a fixed revision (preferred) or ensure your CI/README documents setting -I nixpkgs=<path> before invoking nix-shell.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let shellExpression :: Text =
[fmt|{ pkgs ? import <nixpkgs> {} }:
( (#{nixFile}) { inherit pkgs; } ).env|]
( (#{nixFile}) { inherit pkgs; } ).shell|]
let shellExpression :: Text =
[fmt|let pkgs = import <nixpkgs> {}; in
( (#{nixFile}) { inherit pkgs; } ).shell|]
πŸ€– Prompt for AI Agents
In cli/src/Neo/Shell.hs around lines 78–81, the code currently builds a
top-level lambda that returns a function instead of emitting a derivation;
change the expression so it evaluates directly to the derivation/shell (i.e.,
apply/import nixpkgs and call into the nix file to produce its .shell result
rather than returning a lambda). Replace the lambda-style "{ pkgs ? import
<nixpkgs> {} }:" wrapper with an expression that imports/pins a specific nixpkgs
revision (or uses a passed -I nixpkgs=<path>) and then immediately evaluates
(#{nixFile}).shell so nix-shell -E gets a derivation; also update CI/README to
document the chosen approach if you opt to require -I instead of pinning.

completion <-
Subprocess.openInherit "nix-shell" (Array.fromLinkedList ["-E", shellExpression]) rootFolder Subprocess.InheritBOTH
if completion.exitCode != 0
Expand Down
12 changes: 12 additions & 0 deletions common.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{ pkgs ? import ./nix/nixpkgs.nix { } }:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Thread the compiler choice; do not hard-code what the cosmos may change.

Expose a configurable compiler and pass it down to the project builder; default to a verified choice.

Apply:

-{ pkgs ? import ./nix/nixpkgs.nix { } }:
+{ pkgs ? import ./nix/nixpkgs.nix { }, compilerNixName ? "ghc910" }:
 ...
-in (hp {
+in (hp {
   packages = {
     "nhcore" = ./core;
     "nhcli" = ./cli;
   };
 
   mainPackageName = "nhcli";
   executableName = "nhcli:exe:neo";
-})
+  compilerNixName = compilerNixName;
+})

Also applies to: 10-12

πŸ€– Prompt for AI Agents
In common.nix around lines 1 and 10-12, the file currently hard-codes the
compiler via the imported pkgs; change the top-level attribute set to accept a
configurable compiler argument with a safe default from pkgs (e.g. default to
the verified compiler in pkgs like pkgs.stdenv.cc or the project's validated
compiler), then thread that compiler argument through wherever the project
builder is invoked (lines ~10-12) so the builder uses the passed-in compiler
instead of the hard-coded pkgs value; ensure the parameter name is documented in
the file header and used in all places that construct or call the project
builder.

🧹 Nitpick (assertive)

Eliminate the default pkgs import here to prevent accidental double-pinning.

Since default.nix and shell.nix already pass a pinned pkgs, making pkgs mandatory here avoids mixing nixpkgs instances if someone imports common.nix directly.

Apply:

-{ pkgs ? import ./nix/nixpkgs.nix { }, compilerNixName ? "ghc910" }:
+{ pkgs, compilerNixName ? "ghc910" }:

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In common.nix around line 1, remove the default import so pkgs is a required
argument to avoid accidental double-pinning; change the function header to
accept only pkgs (no default import) and update any call sites to pass the
pinned pkgs from default.nix/shell.nix if they don’t already, ensuring no
implicit import of nixpkgs remains.


let hp = import ./cli/nix/haskell-project.nix { inherit pkgs; };
in (hp {
packages = {
"nhcore" = ./.;
"nhcli" = ./.;
};

mainPackageName = "nhcli";
executableName = "nhcli:exe:neo";
})
2 changes: 1 addition & 1 deletion core/nhcore.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 3.4
name: nhcore
version: 0.9.0
version: 0.9.1
synopsis: Core Library for NeoHaskell
description: NeoHaskell is a dialect of Haskell that is focused on newcomer-friendliness and productivity.
homepage: https://neohaskell.org
Expand Down
3 changes: 2 additions & 1 deletion default.nix
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import ./cli/default.nix
{ pkgs ? import ./nix/nixpkgs.nix { } }:
((import ./common.nix { inherit pkgs; }).package)
12 changes: 8 additions & 4 deletions nix/nixpkgs.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Pinned nixpkgs for NeoHaskell project
# Last updated: 2025-08-25
Comment on lines +1 to +2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Reduce duplication; bind the revision once and derive the name.

This avoids editing multiple spots when bumping nixpkgs.

Apply:

-import (builtins.fetchTarball {
-  name = "nixpkgs-20075955";
-  url = "https://github.com/nixos/nixpkgs/archive/20075955deac2583bb12f07151c2df830ef346b4.tar.gz";
+let
+  rev = "20075955deac2583bb12f07151c2df830ef346b4";
+in import (builtins.fetchTarball {
+  name = "nixpkgs-${rev}";
+  url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";

Also applies to: 5-6

πŸ€– Prompt for AI Agents
In nix/nixpkgs.nix around lines 1-2 (and also apply the same change to lines
5-6), the nixpkgs revision and name are duplicated; bind the revision to a
single variable at the top and derive the package set name from that variable,
then replace all other hard-coded revision/name occurrences to reference those
variables so future bumps only change one value.

# Commit from nixos-unstable branch (2025-08-19)
import (builtins.fetchTarball {
name = "nixpkgs-054144e5";
url =
"https://github.com/nixos/nixpkgs/archive/054144e516ca1e3cf5706d5c3427d350eaa107af.tar.gz";
sha256 = "0ixd2mc9kkmr8mz20ah2j7akgwllxsjv94ah895zgi9b9q1h25ka";
name = "nixpkgs-20075955";
url = "https://github.com/nixos/nixpkgs/archive/20075955deac2583bb12f07151c2df830ef346b4.tar.gz";
# To get the sha256, run:
# nix-prefetch-url --unpack https://github.com/nixos/nixpkgs/archive/20075955deac2583bb12f07151c2df830ef346b4.tar.gz
sha256 = "1s3lxb33cwazlx72pygcbcc76bbgbhdil6q9bhqbzbjxj001zk0w";
})
63 changes: 38 additions & 25 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,38 +45,51 @@ if [[ $REPLY =~ ^[Nn]$ ]]; then
else
echo "πŸ”§ Adding binary cache configuration..."

# Create /etc/nix/nix.conf if it doesn't exist
if [ ! -f /etc/nix/nix.conf ]; then
echo "Creating /etc/nix/nix.conf..."
sudo mkdir -p /etc/nix
sudo touch /etc/nix/nix.conf
# Add binary cache configuration to custom config file
sudo mkdir -p /etc/nix
if [ ! -f /etc/nix/nix.custom.conf ]; then
echo "Creating /etc/nix/nix.custom.conf..."
sudo touch /etc/nix/nix.custom.conf
fi

# Check if the substituters line exists
if sudo grep -q "^extra-substituters" /etc/nix/nix.conf; then
# Add to existing line if not already present
if ! sudo grep -q "$BINARY_CACHE_SUBSTITUTERS" /etc/nix/nix.conf; then
escaped_substituters=$(printf '%s\n' "$BINARY_CACHE_SUBSTITUTERS" | sed 's/[[\.*^$()+?{|]/\\&/g')
sudo sed -i "/^extra-substituters/s/$/ $escaped_substituters/" /etc/nix/nix.conf
fi
else
# Add new line
echo "extra-substituters = $BINARY_CACHE_SUBSTITUTERS" | sudo tee -a /etc/nix/nix.conf > /dev/null
# Append our config if not already present
if ! sudo grep -q "$BINARY_CACHE_SUBSTITUTERS" /etc/nix/nix.custom.conf; then
echo "extra-substituters = $BINARY_CACHE_SUBSTITUTERS" | sudo tee -a /etc/nix/nix.custom.conf > /dev/null
fi
if ! sudo grep -q "$BINARY_CACHE_PUBLIC_KEYS" /etc/nix/nix.custom.conf; then
echo "extra-trusted-public-keys = $BINARY_CACHE_PUBLIC_KEYS" | sudo tee -a /etc/nix/nix.custom.conf > /dev/null
fi

# Check if the trusted-public-keys line exists
if sudo grep -q "^extra-trusted-public-keys" /etc/nix/nix.conf; then
# Add to existing line if not already present
if ! sudo grep -q "$BINARY_CACHE_PUBLIC_KEYS" /etc/nix/nix.conf; then
escaped_keys=$(printf '%s\n' "$BINARY_CACHE_PUBLIC_KEYS" | sed 's/[[\.*^$()+?{|]/\\&/g')
sudo sed -i "/^extra-trusted-public-keys/s/$/ $escaped_keys/" /etc/nix/nix.conf
echo "βœ… Binary cache configuration added successfully!"

# Restart nix daemon to pick up new configuration
echo "πŸ”„ Restarting Nix daemon to apply configuration changes..."

# Detect platform and restart daemon accordingly
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
if sudo launchctl kickstart -k system/org.nixos.nix-daemon 2>/dev/null; then
echo "βœ… Nix daemon restarted successfully on macOS"
else
echo "⚠️ Failed to restart nix daemon on macOS - configuration will take effect on next restart"
fi
elif command -v systemctl &> /dev/null; then
# Linux with systemd (including WSL with systemd)
if sudo systemctl restart nix-daemon.service 2>/dev/null; then
echo "βœ… Nix daemon restarted successfully"
else
echo "⚠️ Failed to restart nix daemon - configuration will take effect on next restart"
fi
else
# Add new line
echo "extra-trusted-public-keys = $BINARY_CACHE_PUBLIC_KEYS" | sudo tee -a /etc/nix/nix.conf > /dev/null
# WSL or Linux without systemd - try pkill approach
echo "πŸ”„ Attempting to restart nix daemon..."
if sudo pkill nix-daemon 2>/dev/null; then
echo "βœ… Nix daemon stopped - it will restart automatically on next use"
else
echo "⚠️ Cannot automatically restart nix daemon on this system"
echo " Configuration will take effect when you restart your shell or reboot"
fi
fi
Comment on lines +76 to 92

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add a fallback when systemctl restart fails.

On some distros/WSL with systemd present but not fully functional, systemctl may exist yet fail. Fall back to pkill in that branch to avoid a no-op.

Apply this refinement:

@@
-    elif command -v systemctl &> /dev/null; then
+    elif command -v systemctl &> /dev/null; then
         # Linux with systemd (including WSL with systemd)
-        if sudo systemctl restart nix-daemon.service 2>/dev/null; then
+        if sudo systemctl restart nix-daemon.service 2>/dev/null; then
             echo "βœ… Nix daemon restarted successfully"
         else
-            echo "⚠️  Failed to restart nix daemon - configuration will take effect on next restart"
+            echo "⚠️  Failed to restart nix daemon via systemd - attempting manual stop so it auto-restarts"
+            if sudo pkill nix-daemon 2>/dev/null; then
+                echo "βœ… Nix daemon stopped - it will restart automatically on next use"
+            else
+                echo "⚠️  Cannot automatically restart nix daemon on this system"
+                echo "    Configuration will take effect when you restart your shell or reboot"
+            fi
         fi
πŸ€– Prompt for AI Agents
In scripts/install.sh around lines 76 to 92, the current branch tries systemctl
restart and on failure prints a warning but does not attempt the pkill fallback;
update the logic so that if sudo systemctl restart nix-daemon.service fails
(exit non-zero) you then attempt the pkill fallback (sudo pkill nix-daemon
2>/dev/null) and emit the appropriate success/failure messages based on pkill's
result, keeping existing messages and exit-code behavior and ensuring output
distinguishes "systemctl failed, falling back to pkill" versus final failure.


echo "βœ… Binary cache configuration added successfully!"
fi

echo ""
Expand Down
59 changes: 1 addition & 58 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -1,58 +1 @@
{ pkgs ? import ./nix/nixpkgs.nix { }, }:
let
nix-pre-commit-hooks = import (builtins.fetchTarball
"https://github.com/cachix/git-hooks.nix/tarball/master");
in let
pre-commit = {
# Configured with the module options defined in `modules/pre-commit.nix`:
pre-commit-check = nix-pre-commit-hooks.run {
src = ./.;
# If your hooks are intrusive, avoid running on each commit with a default_states like this:
# default_stages = ["manual" "pre-push"];
hooks = {
fourmolu.enable = true;
nixfmt-classic.enable = true;
};
};
};
in pkgs.mkShell rec {
buildInputs = [
# Haskell dev tools
pkgs.ghc
pkgs.cabal-install
pkgs.haskell-language-server
pkgs.fourmolu
pkgs.hlint
pkgs.haskellPackages.zlib
pkgs.haskellPackages.hspec-discover
pkgs.haskellPackages.doctest

# Nix dev tools
pkgs.nil
pkgs.nixfmt-classic
pkgs.nixpkgs-fmt

# Website
pkgs.nodejs

# Required native libs
pkgs.pkg-config
pkgs.zlib
] ++ pre-commit.pre-commit-check.enabledPackages;

shellHook = ''
unset TEMP TMP TEMPDIR TMPDIR # Required for nix-shell to work
${pre-commit.pre-commit-check.shellHook}

run-doctests-verbose() {
cabal repl --with-compiler=doctest --repl-options=--verbose core
}

run-doctests() {
cabal repl --with-compiler=doctest core
}
'';

# Required for cabal to find the location of zlib and other native libraries
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
}
{ pkgs ? import ./nix/nixpkgs.nix { } }: ((import ./common.nix { inherit pkgs; }).shell)
Loading