diff --git a/maintainers/flake-module.nix b/maintainers/flake-module.nix
index f18a06f9d..bdc0d49a9 100644
--- a/maintainers/flake-module.nix
+++ b/maintainers/flake-module.nix
@@ -99,7 +99,6 @@
               ''^src/libexpr/nixexpr\.cc$''
               ''^src/libexpr/nixexpr\.hh$''
               ''^src/libexpr/parser-state\.hh$''
-              ''^src/libexpr/pos-table\.hh$''
               ''^src/libexpr/primops\.cc$''
               ''^src/libexpr/primops\.hh$''
               ''^src/libexpr/primops/context\.cc$''
diff --git a/src/libexpr/meson.build b/src/libexpr/meson.build
index 4d8a38b43..7b5557bd1 100644
--- a/src/libexpr/meson.build
+++ b/src/libexpr/meson.build
@@ -175,8 +175,6 @@ headers = [config_h] + files(
   # internal: 'lexer-helpers.hh',
   'nixexpr.hh',
   'parser-state.hh',
-  'pos-idx.hh',
-  'pos-table.hh',
   'primops.hh',
   'print-ambiguous.hh',
   'print-options.hh',
diff --git a/src/libexpr/nixexpr.cc b/src/libexpr/nixexpr.cc
index dbc74faf9..5e93b2a57 100644
--- a/src/libexpr/nixexpr.cc
+++ b/src/libexpr/nixexpr.cc
@@ -601,41 +601,6 @@ void ExprLambda::setDocComment(DocComment docComment) {
     }
 };
 
-
-
-/* Position table. */
-
-Pos PosTable::operator[](PosIdx p) const
-{
-    auto origin = resolve(p);
-    if (!origin)
-        return {};
-
-    const auto offset = origin->offsetOf(p);
-
-    Pos result{0, 0, origin->origin};
-    auto lines = this->lines.lock();
-    auto linesForInput = (*lines)[origin->offset];
-
-    if (linesForInput.empty()) {
-        auto source = result.getSource().value_or("");
-        const char * begin = source.data();
-        for (Pos::LinesIterator it(source), end; it != end; it++)
-            linesForInput.push_back(it->data() - begin);
-        if (linesForInput.empty())
-            linesForInput.push_back(0);
-    }
-    // as above: the first line starts at byte 0 and is always present
-    auto lineStartOffset = std::prev(
-        std::upper_bound(linesForInput.begin(), linesForInput.end(), offset));
-
-    result.line = 1 + (lineStartOffset - linesForInput.begin());
-    result.column = 1 + (offset - *lineStartOffset);
-    return result;
-}
-
-
-
 /* Symbol table. */
 
 size_t SymbolTable::totalSize() const
diff --git a/src/libutil/meson.build b/src/libutil/meson.build
index cba5a5288..ea38d6de4 100644
--- a/src/libutil/meson.build
+++ b/src/libutil/meson.build
@@ -146,6 +146,7 @@ sources = files(
   'logging.cc',
   'memory-source-accessor.cc',
   'position.cc',
+  'pos-table.cc',
   'posix-source-accessor.cc',
   'references.cc',
   'serialise.cc',
@@ -207,6 +208,8 @@ headers = [config_h] + files(
   'memory-source-accessor.hh',
   'muxable-pipe.hh',
   'pool.hh',
+  'pos-idx.hh',
+  'pos-table.hh',
   'position.hh',
   'posix-source-accessor.hh',
   'processes.hh',
diff --git a/src/libexpr/pos-idx.hh b/src/libutil/pos-idx.hh
similarity index 98%
rename from src/libexpr/pos-idx.hh
rename to src/libutil/pos-idx.hh
index 2faa6b7fe..c1749ba69 100644
--- a/src/libexpr/pos-idx.hh
+++ b/src/libutil/pos-idx.hh
@@ -1,4 +1,5 @@
 #pragma once
+///@file
 
 #include <cinttypes>
 #include <functional>
diff --git a/src/libutil/pos-table.cc b/src/libutil/pos-table.cc
new file mode 100644
index 000000000..8178beb90
--- /dev/null
+++ b/src/libutil/pos-table.cc
@@ -0,0 +1,37 @@
+#include "pos-table.hh"
+
+#include <algorithm>
+
+namespace nix {
+
+/* Position table. */
+
+Pos PosTable::operator[](PosIdx p) const
+{
+    auto origin = resolve(p);
+    if (!origin)
+        return {};
+
+    const auto offset = origin->offsetOf(p);
+
+    Pos result{0, 0, origin->origin};
+    auto lines = this->lines.lock();
+    auto linesForInput = (*lines)[origin->offset];
+
+    if (linesForInput.empty()) {
+        auto source = result.getSource().value_or("");
+        const char * begin = source.data();
+        for (Pos::LinesIterator it(source), end; it != end; it++)
+            linesForInput.push_back(it->data() - begin);
+        if (linesForInput.empty())
+            linesForInput.push_back(0);
+    }
+    // as above: the first line starts at byte 0 and is always present
+    auto lineStartOffset = std::prev(std::upper_bound(linesForInput.begin(), linesForInput.end(), offset));
+
+    result.line = 1 + (lineStartOffset - linesForInput.begin());
+    result.column = 1 + (offset - *lineStartOffset);
+    return result;
+}
+
+}
diff --git a/src/libexpr/pos-table.hh b/src/libutil/pos-table.hh
similarity index 94%
rename from src/libexpr/pos-table.hh
rename to src/libutil/pos-table.hh
index ba2b91cf3..673cf62ae 100644
--- a/src/libexpr/pos-table.hh
+++ b/src/libutil/pos-table.hh
@@ -1,4 +1,5 @@
 #pragma once
+///@file
 
 #include <cstdint>
 #include <vector>
@@ -18,9 +19,12 @@ public:
     private:
         uint32_t offset;
 
-        Origin(Pos::Origin origin, uint32_t offset, size_t size):
-            offset(offset), origin(origin), size(size)
-        {}
+        Origin(Pos::Origin origin, uint32_t offset, size_t size)
+            : offset(offset)
+            , origin(origin)
+            , size(size)
+        {
+        }
 
     public:
         const Pos::Origin origin;