git.iko.soy/rowtype-yoga/purslint
on main help sign in
Port LSP server to JavaScript for 2-8x speedup

Performance comparison (document lint time): - 69 lines: 361ms (Go) vs 159ms (Node) - 2.3x faster - 346 lines: 1394ms (Go) vs 247ms (Node) - 5.6x faster - 543 lines: 2553ms (Go) vs 337ms (Node) - 7.6x faster Changes: - Add bin/purelint-lsp.js entry point - Create JS FFI for LSP.Stdio (synchronous stdin reading) - Create JS FFI for Purelint.LSP (mkNull, hasKey, getKey) - Rename Node.Process -> Node.CLI to avoid conflict with node-process - Rename Node.FS.Sync -> Node.FS.Simple to avoid conflict with node-fs - Build CLI and LSP to separate output directories ๐Ÿ’˜ Generated with Crush Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>

commit12820deae7110edfe3e22a6943afba6b74475774 authorMark <mark.eibes@gmail.com> dateSun 25 Jan 2026, 19:10 (8 months ago) parentf92d2c8
11 files changed+454โˆ’7
bin/purelint-cli/src/Main.purs+2โˆ’2 bin/purelint-cli/src/Node/CLI.js+4 bin/purelint-cli/src/Node/Process.purs โ†’ bin/purelint-cli/src/Node/CLI.purs+1โˆ’1 bin/purelint-cli/src/Node/FS/Simple.js+22 bin/purelint-cli/src/Node/FS/Sync.purs โ†’ bin/purelint-cli/src/Node/FS/Simple.purs+1โˆ’1 bin/purelint-lsp.js+3 bin/purelint-lsp/src/LSP.js+14 bin/purelint-lsp/src/LSP/Stdio.js+87 bin/purelint.js+1โˆ’1 package.json+8โˆ’2 spago.lock+311
bin/purelint-cli/src/Main.purs+2โˆ’2
@@ -10,8 +10,8 @@ import Data.Newtype (unwrap)
1010import Data.String.CodeUnits as String
1111import Effect (Effect)
1212import Effect.Console (log)
13โˆ’import Node.FS.Sync as FS
14โˆ’import Node.Process as Process
13+import Node.FS.Simple as FS
14+import Node.CLI as Process
1515import Purelint.Config (defaultConfig, filterRules, parseConfig)
1616import Purelint.Fix (applyAllFixes)
1717import Purelint.Rule (Rule)
bin/purelint-cli/src/Node/CLI.jsnew file+4
@@ -0,0 +1,4 @@
1+// Node.CLI FFI for JavaScript
2+export const argv = () => process.argv;
3+export const cwd = () => process.cwd();
4+export const exit = (code) => () => process.exit(code);
bin/purelint-cli/src/Node/Process.purs โ†’ bin/purelint-cli/src/Node/CLI.pursrenamed+1โˆ’1
@@ -1,4 +1,4 @@
1โˆ’module Node.Process where
1+module Node.CLI where
22
33import Prelude
44import Effect (Effect)
bin/purelint-cli/src/Node/FS/Simple.jsnew file+22
@@ -0,0 +1,22 @@
1+// Node.FS.Simple FFI for JavaScript
2+import * as fs from 'fs';
3+
4+export const readTextFile = (path) => () => fs.readFileSync(path, 'utf8');
5+
6+export const writeTextFile = (path) => (content) => () => {
7+ fs.writeFileSync(path, content, 'utf8');
8+};
9+
10+export const exists = (path) => () => fs.existsSync(path);
11+
12+export const readdir = (path) => () => fs.readdirSync(path);
13+
14+export const stat = (path) => () => {
15+ const s = fs.statSync(path);
16+ return {
17+ isFile: s.isFile(),
18+ isDirectory: s.isDirectory(),
19+ isSymbolicLink: s.isSymbolicLink(),
20+ size: s.size
21+ };
22+};
bin/purelint-cli/src/Node/FS/Sync.purs โ†’ bin/purelint-cli/src/Node/FS/Simple.pursrenamed+1โˆ’1
@@ -1,4 +1,4 @@
1โˆ’module Node.FS.Sync where
1+module Node.FS.Simple where
22
33import Prelude
44import Effect (Effect)
bin/purelint-lsp.jsnew file+3
@@ -0,0 +1,3 @@
1+#!/usr/bin/env node
2+import { main } from "../output-lsp/Main/index.js";
3+main();
bin/purelint-lsp/src/LSP.jsnew file+14
@@ -0,0 +1,14 @@
1+// Purelint.LSP FFI for JavaScript
2+
3+// Create a null value
4+export const mkNull = (_) => null;
5+
6+// Check if a key exists in an object
7+export const hasKey = (key) => (obj) => {
8+ return obj != null && typeof obj === 'object' && key in obj;
9+};
10+
11+// Get a key from an object
12+export const getKey = (key) => (obj) => {
13+ return obj[key];
14+};
bin/purelint-lsp/src/LSP/Stdio.jsnew file+87
@@ -0,0 +1,87 @@
1+// LSP stdio FFI for JavaScript
2+import * as fs from 'fs';
3+import * as $Maybe from '../Data.Maybe/index.js';
4+
5+// Buffer for reading LSP messages
6+let buffer = '';
7+
8+// Try to parse a complete LSP message from buffer
9+function tryParseMessage() {
10+ // Look for Content-Length header
11+ const headerMatch = buffer.match(/Content-Length:\s*(\d+)\r?\n\r?\n/);
12+ if (!headerMatch) return null;
13+
14+ const headerEnd = headerMatch.index + headerMatch[0].length;
15+ const contentLength = parseInt(headerMatch[1], 10);
16+
17+ // Check if we have the full content
18+ if (buffer.length < headerEnd + contentLength) return null;
19+
20+ // Extract the message content
21+ const content = buffer.slice(headerEnd, headerEnd + contentLength);
22+ buffer = buffer.slice(headerEnd + contentLength);
23+
24+ return content;
25+}
26+
27+// Read a message synchronously (blocking)
28+// Returns Maybe String
29+export const readMessage = () => {
30+ // First check if we already have a complete message in buffer
31+ let msg = tryParseMessage();
32+ if (msg !== null) {
33+ return $Maybe.Just.create(msg);
34+ }
35+
36+ // Need to read more data synchronously from stdin
37+ const fd = fs.openSync('/dev/stdin', 'r');
38+
39+ try {
40+ while (true) {
41+ const chunk = Buffer.alloc(4096);
42+ const bytesRead = fs.readSync(fd, chunk, 0, 4096);
43+
44+ if (bytesRead === 0) {
45+ // EOF
46+ return $Maybe.Nothing.value;
47+ }
48+
49+ buffer += chunk.toString('utf8', 0, bytesRead);
50+
51+ msg = tryParseMessage();
52+ if (msg !== null) {
53+ return $Maybe.Just.create(msg);
54+ }
55+ }
56+ } catch (e) {
57+ return $Maybe.Nothing.value;
58+ } finally {
59+ fs.closeSync(fd);
60+ }
61+};
62+
63+// Write a JSON-RPC message to stdout
64+export const writeMessage = (msg) => () => {
65+ const content = msg;
66+ const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
67+ process.stdout.write(header + content);
68+};
69+
70+// Log a message to stderr (for debugging)
71+export const logMessage = (msg) => () => {
72+ process.stderr.write('[purelint-lsp] ' + msg + '\n');
73+};
74+
75+// Parse JSON string to Foreign (returns Maybe Foreign)
76+export const parseJSON = (str) => {
77+ try {
78+ return $Maybe.Just.create(JSON.parse(str));
79+ } catch (e) {
80+ return $Maybe.Nothing.value;
81+ }
82+};
83+
84+// Stringify Foreign to JSON
85+export const stringifyJSON = (obj) => {
86+ return JSON.stringify(obj);
87+};
bin/purelint.js+1โˆ’1
@@ -1,3 +1,3 @@
11#!/usr/bin/env node
2โˆ’import { main } from "../output/Main/index.js";
2+import { main } from "../output-cli/Main/index.js";
33main();
package.json+8โˆ’2
@@ -11,6 +11,12 @@
1111 "build": "purs compile 'lib/**/*.purs' 'bin/purelint-cli/**/*.purs' '.spago/**/*.purs'",
1212 "test": "node --experimental-vm-modules test.js"
1313 },
14โˆ’ "keywords": ["purescript", "linter"],
15โˆ’ "license": "MIT"
14+ "keywords": [
15+ "purescript",
16+ "linter"
17+ ],
18+ "license": "MIT",
19+ "dependencies": {
20+ "spago": "^0.93.45"
21+ }
1622}
spago.lock+311
@@ -80,11 +80,16 @@
8080 "foldable-traversable",
8181 "maybe",
8282 "newtype",
83+ "node-buffer",
84+ "node-fs",
85+ "node-process",
8386 "prelude",
8487 "purelint",
8588 "strings"
8689 ],
8790 "build_plan": [
91+ "aff",
92+ "arraybuffer-types",
8893 "arrays",
8994 "bifunctors",
9095 "catenable-lists",
@@ -92,6 +97,7 @@
9297 "const",
9398 "contravariant",
9499 "control",
100+ "datetime",
95101 "distributive",
96102 "effect",
97103 "either",
@@ -99,6 +105,8 @@
99105 "exceptions",
100106 "exists",
101107 "foldable-traversable",
108+ "foreign",
109+ "foreign-object",
102110 "free",
103111 "functions",
104112 "functors",
@@ -106,16 +114,27 @@
106114 "identity",
107115 "integers",
108116 "invariant",
117+ "js-date",
109118 "language-cst-parser",
110119 "lazy",
111120 "lists",
112121 "maybe",
113122 "newtype",
123+ "node-buffer",
124+ "node-event-emitter",
125+ "node-fs",
126+ "node-path",
127+ "node-process",
128+ "node-streams",
114129 "nonempty",
130+ "now",
131+ "nullable",
115132 "numbers",
116133 "ordered-collections",
117134 "orders",
135+ "parallel",
118136 "partial",
137+ "posix-types",
119138 "prelude",
120139 "profunctor",
121140 "purelint",
@@ -203,6 +222,72 @@
203222 "dependencies": [],
204223 "build_plan": []
205224 }
225+ },
226+ "purelint-tests": {
227+ "path": "./",
228+ "core": {
229+ "dependencies": [
230+ "arrays",
231+ "console",
232+ "effect",
233+ "either",
234+ "maybe",
235+ "newtype",
236+ "prelude",
237+ "purelint"
238+ ],
239+ "build_plan": [
240+ "arrays",
241+ "bifunctors",
242+ "catenable-lists",
243+ "console",
244+ "const",
245+ "contravariant",
246+ "control",
247+ "distributive",
248+ "effect",
249+ "either",
250+ "enums",
251+ "exceptions",
252+ "exists",
253+ "foldable-traversable",
254+ "free",
255+ "functions",
256+ "functors",
257+ "gen",
258+ "identity",
259+ "integers",
260+ "invariant",
261+ "language-cst-parser",
262+ "lazy",
263+ "lists",
264+ "maybe",
265+ "newtype",
266+ "nonempty",
267+ "numbers",
268+ "ordered-collections",
269+ "orders",
270+ "partial",
271+ "prelude",
272+ "profunctor",
273+ "purelint",
274+ "refs",
275+ "safe-coerce",
276+ "st",
277+ "strings",
278+ "tailrec",
279+ "transformers",
280+ "tuples",
281+ "type-equality",
282+ "typelevel-prelude",
283+ "unfoldable",
284+ "unsafe-coerce"
285+ ]
286+ },
287+ "test": {
288+ "dependencies": [],
289+ "build_plan": []
290+ }
206291 }
207292 },
208293 "package_set": {
@@ -784,6 +869,35 @@
784869 "extra_packages": {}
785870 },
786871 "packages": {
872+ "aff": {
873+ "type": "registry",
874+ "version": "8.0.0",
875+ "integrity": "sha256-5MmdI4+0RHBtSBy+YlU3/Cq4R5W2ih3OaRedJIrVHdk=",
876+ "dependencies": [
877+ "bifunctors",
878+ "control",
879+ "datetime",
880+ "effect",
881+ "either",
882+ "exceptions",
883+ "foldable-traversable",
884+ "functions",
885+ "maybe",
886+ "newtype",
887+ "parallel",
888+ "prelude",
889+ "refs",
890+ "tailrec",
891+ "transformers",
892+ "unsafe-coerce"
893+ ]
894+ },
895+ "arraybuffer-types": {
896+ "type": "registry",
897+ "version": "3.0.2",
898+ "integrity": "sha256-mQKokysYVkooS4uXbO+yovmV/s8b138Ws3zQvOwIHRA=",
899+ "dependencies": []
900+ },
787901 "arrays": {
788902 "type": "registry",
789903 "version": "7.3.0",
@@ -871,6 +985,29 @@
871985 "prelude"
872986 ]
873987 },
988+ "datetime": {
989+ "type": "registry",
990+ "version": "6.1.0",
991+ "integrity": "sha256-g/5X5BBegQWLpI9IWD+sY6mcaYpzzlW5lz5NBzaMtyI=",
992+ "dependencies": [
993+ "bifunctors",
994+ "control",
995+ "either",
996+ "enums",
997+ "foldable-traversable",
998+ "functions",
999+ "gen",
1000+ "integers",
1001+ "lists",
1002+ "maybe",
1003+ "newtype",
1004+ "numbers",
1005+ "ordered-collections",
1006+ "partial",
1007+ "prelude",
1008+ "tuples"
1009+ ]
1010+ },
8741011 "distributive": {
8751012 "type": "registry",
8761013 "version": "6.0.0",
@@ -972,6 +1109,25 @@
9721109 "transformers"
9731110 ]
9741111 },
1112+ "foreign-object": {
1113+ "type": "registry",
1114+ "version": "4.1.0",
1115+ "integrity": "sha256-q24okj6mT+yGHYQ+ei/pYPj5ih6sTbu7eDv/WU56JVo=",
1116+ "dependencies": [
1117+ "arrays",
1118+ "foldable-traversable",
1119+ "functions",
1120+ "gen",
1121+ "lists",
1122+ "maybe",
1123+ "prelude",
1124+ "st",
1125+ "tailrec",
1126+ "tuples",
1127+ "typelevel-prelude",
1128+ "unfoldable"
1129+ ]
1130+ },
9751131 "free": {
9761132 "type": "registry",
9771133 "version": "7.1.0",
@@ -1068,6 +1224,19 @@
10681224 "prelude"
10691225 ]
10701226 },
1227+ "js-date": {
1228+ "type": "registry",
1229+ "version": "8.0.0",
1230+ "integrity": "sha256-6TVF4DWg5JL+jRAsoMssYw8rgOVALMUHT1CuNZt8NRo=",
1231+ "dependencies": [
1232+ "datetime",
1233+ "effect",
1234+ "exceptions",
1235+ "foreign",
1236+ "integers",
1237+ "now"
1238+ ]
1239+ },
10711240 "language-cst-parser": {
10721241 "type": "registry",
10731242 "version": "0.14.1",
@@ -1152,6 +1321,102 @@
11521321 "safe-coerce"
11531322 ]
11541323 },
1324+ "node-buffer": {
1325+ "type": "registry",
1326+ "version": "9.0.0",
1327+ "integrity": "sha256-PWE2DJ5ruBLCmeA/fUiuySEFmUJ/VuRfyrnCuVZBlu4=",
1328+ "dependencies": [
1329+ "arraybuffer-types",
1330+ "effect",
1331+ "maybe",
1332+ "nullable",
1333+ "st",
1334+ "unsafe-coerce"
1335+ ]
1336+ },
1337+ "node-event-emitter": {
1338+ "type": "registry",
1339+ "version": "3.0.0",
1340+ "integrity": "sha256-Qw0MjsT4xRH2j2i4K8JmRjcMKnH5z1Cw39t00q4LE4w=",
1341+ "dependencies": [
1342+ "effect",
1343+ "either",
1344+ "functions",
1345+ "maybe",
1346+ "nullable",
1347+ "prelude",
1348+ "unsafe-coerce"
1349+ ]
1350+ },
1351+ "node-fs": {
1352+ "type": "registry",
1353+ "version": "9.2.0",
1354+ "integrity": "sha256-Sg0vkXycEzkEerX6hLccz21Ygd9w1+QSk1thotRZPGI=",
1355+ "dependencies": [
1356+ "datetime",
1357+ "effect",
1358+ "either",
1359+ "enums",
1360+ "exceptions",
1361+ "functions",
1362+ "integers",
1363+ "js-date",
1364+ "maybe",
1365+ "node-buffer",
1366+ "node-path",
1367+ "node-streams",
1368+ "nullable",
1369+ "partial",
1370+ "prelude",
1371+ "strings",
1372+ "unsafe-coerce"
1373+ ]
1374+ },
1375+ "node-path": {
1376+ "type": "registry",
1377+ "version": "5.0.1",
1378+ "integrity": "sha256-ePOElFamHkffhwJcS0Ozq4A14rflnkasFU6X2B8/yXs=",
1379+ "dependencies": [
1380+ "effect"
1381+ ]
1382+ },
1383+ "node-process": {
1384+ "type": "registry",
1385+ "version": "11.2.0",
1386+ "integrity": "sha256-+2MQDYChjGbVbapCyJtuWYwD41jk+BntF/kcOTKBMVs=",
1387+ "dependencies": [
1388+ "effect",
1389+ "foreign",
1390+ "foreign-object",
1391+ "maybe",
1392+ "node-event-emitter",
1393+ "node-streams",
1394+ "posix-types",
1395+ "prelude",
1396+ "unsafe-coerce"
1397+ ]
1398+ },
1399+ "node-streams": {
1400+ "type": "registry",
1401+ "version": "9.0.1",
1402+ "integrity": "sha256-7RJ6RqjOlhW+QlDFQNUHlkCG/CuYTTLT8yary5jhhsU=",
1403+ "dependencies": [
1404+ "aff",
1405+ "arrays",
1406+ "effect",
1407+ "either",
1408+ "exceptions",
1409+ "maybe",
1410+ "node-buffer",
1411+ "node-event-emitter",
1412+ "nullable",
1413+ "prelude",
1414+ "refs",
1415+ "st",
1416+ "tailrec",
1417+ "unsafe-coerce"
1418+ ]
1419+ },
11551420 "nonempty": {
11561421 "type": "registry",
11571422 "version": "7.0.0",
@@ -1165,6 +1430,25 @@
11651430 "unfoldable"
11661431 ]
11671432 },
1433+ "now": {
1434+ "type": "registry",
1435+ "version": "6.0.0",
1436+ "integrity": "sha256-xZ7x37ZMREfs6GCDw/h+FaKHV/3sPWmtqBZRGTxybQY=",
1437+ "dependencies": [
1438+ "datetime",
1439+ "effect"
1440+ ]
1441+ },
1442+ "nullable": {
1443+ "type": "registry",
1444+ "version": "6.0.0",
1445+ "integrity": "sha256-yiGBVl3AD+Guy4kNWWeN+zl1gCiJK+oeIFtZtPCw4+o=",
1446+ "dependencies": [
1447+ "effect",
1448+ "functions",
1449+ "maybe"
1450+ ]
1451+ },
11681452 "numbers": {
11691453 "type": "registry",
11701454 "version": "9.0.1",
@@ -1201,12 +1485,39 @@
12011485 "prelude"
12021486 ]
12031487 },
1488+ "parallel": {
1489+ "type": "registry",
1490+ "version": "7.0.0",
1491+ "integrity": "sha256-gUC9i4Txnx9K9RcMLsjujbwZz6BB1bnE2MLvw4GIw5o=",
1492+ "dependencies": [
1493+ "control",
1494+ "effect",
1495+ "either",
1496+ "foldable-traversable",
1497+ "functors",
1498+ "maybe",
1499+ "newtype",
1500+ "prelude",
1501+ "profunctor",
1502+ "refs",
1503+ "transformers"
1504+ ]
1505+ },
12041506 "partial": {
12051507 "type": "registry",
12061508 "version": "4.0.0",
12071509 "integrity": "sha256-fwXerld6Xw1VkReh8yeQsdtLVrjfGiVuC5bA1Wyo/J4=",
12081510 "dependencies": []
12091511 },
1512+ "posix-types": {
1513+ "type": "registry",
1514+ "version": "6.0.0",
1515+ "integrity": "sha256-ZfFz8RR1lee/o/Prccyeut3Q+9tYd08mlR72sIh6GzA=",
1516+ "dependencies": [
1517+ "maybe",
1518+ "prelude"
1519+ ]
1520+ },
12101521 "prelude": {
12111522 "type": "registry",
12121523 "version": "6.0.2",