Skip to content

Commit b4d06de

Browse files
committed
Fix pony-lint and pony-lsp on Windows
pony-lint's .gitignore and .ignore pattern matching failed on Windows because path separator handling was hardcoded to /. On Windows, where paths use \, ignore rules were silently ineffective. pony-lsp's JSON-RPC initialization failed on Windows because filesystem paths containing backslashes were embedded directly into JSON strings, producing invalid escape sequences. The LSP file URI conversion also didn't handle Windows drive-letter paths correctly. Also adds Windows CI for tool tests and fixes a flaky workspace scanner test that depended on filesystem enumeration order.
1 parent 8312f60 commit b4d06de

File tree

11 files changed

+237
-32
lines changed

11 files changed

+237
-32
lines changed

.github/workflows/pr-tools.yml

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ permissions:
1919
packages: read
2020

2121
jobs:
22-
tools:
22+
linux:
2323
if: github.event.pull_request.draft == false
2424
runs-on: ubuntu-latest
25-
name: Tools
25+
name: Linux
2626
container:
2727
image: ghcr.io/ponylang/ponyc-ci-alpine3.23-builder:20260201
2828
options: --user pony
@@ -52,3 +52,36 @@ jobs:
5252
run: make test-pony-lsp config=debug
5353
- name: Lint pony-lint
5454
run: make lint-pony-lint config=debug
55+
56+
windows:
57+
if: github.event.pull_request.draft == false
58+
runs-on: windows-2025
59+
defaults:
60+
run:
61+
shell: pwsh
62+
63+
name: Windows
64+
steps:
65+
- name: Checkout
66+
uses: actions/checkout@v6.0.2
67+
- name: Restore Libs Cache
68+
id: restore-libs
69+
uses: actions/cache/restore@v5.0.3
70+
with:
71+
path: |
72+
build/libs
73+
lib/llvm/src/compiler-rt/lib/builtins
74+
key: libs-windows-2025-${{ hashFiles('make.ps1', 'CMakeLists.txt', 'lib/CMakeLists.txt', 'lib/llvm/patches/*') }}
75+
- name: Build Libs
76+
if: steps.restore-libs.outputs.cache-hit != 'true'
77+
run: .\make.ps1 -Command libs
78+
- name: Build
79+
run: |
80+
.\make.ps1 -Command configure -Config Debug
81+
.\make.ps1 -Command build -Config Debug
82+
- name: Test pony-doc
83+
run: .\make.ps1 -Command test -Config Debug -TestsToRun pony-doc-tests
84+
- name: Test pony-lint
85+
run: .\make.ps1 -Command test -Config Debug -TestsToRun pony-lint-tests
86+
- name: Test pony-lsp
87+
run: .\make.ps1 -Command test -Config Debug -TestsToRun pony-lsp-tests

.release-notes/next-release.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ The new fix addresses the root cause: IOCP completion callbacks can fire on Wind
77
Each ASIO event now allocates a small shared liveness token (`iocp_token_t`) containing an atomic dead flag and a reference count. Every in-flight IOCP operation holds a pointer to the token and increments the reference count. When `pony_asio_event_destroy` runs, it sets the dead flag (release store) before freeing the event. Completion callbacks check the dead flag (acquire load) before touching the event — if dead, they clean up the IOCP operation struct without accessing the freed event. The last callback to decrement the reference count to zero frees the token.
88

99
This correctly handles all error codes and all IOCP operation types (connect, accept, send, recv) without swallowing events the actor needs to see.
10+
11+
## Fix pony-lint ignore matching on Windows
12+
13+
pony-lint's `.gitignore` and `.ignore` pattern matching failed on Windows because path separator handling was hardcoded to `/`. On Windows, where paths use `\`, ignore rules were silently ineffective — files that should have been skipped were linted, and anchored patterns like `src/build/` never matched. Windows CI for tool tests has been added to prevent regressions.
14+
15+
## Fix pony-lsp on Windows
16+
17+
pony-lsp's JSON-RPC initialization failed on Windows because filesystem paths containing backslashes were embedded directly into JSON strings, producing invalid escape sequences. The LSP file URI conversion also didn't handle Windows drive-letter paths correctly. Windows CI for tool tests has been added to prevent regressions.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ All notable changes to the Pony compiler and standard library will be documented
2828
- Fix `#share` capability constraint intersection ([PR #4998](https://github.com/ponylang/ponyc/pull/4998))
2929
- Fix use-after-free crash in IOCP runtime on Windows ([PR #5046](https://github.com/ponylang/ponyc/pull/5046))
3030
- Fix pony_os_ip_string returning NULL for valid IP addresses ([PR #5049](https://github.com/ponylang/ponyc/pull/5049))
31+
- Fix pony-lint ignore matching on Windows ([PR #5050](https://github.com/ponylang/ponyc/pull/5050))
32+
- Fix pony-lsp on Windows ([PR #5050](https://github.com/ponylang/ponyc/pull/5050))
3133

3234
### Changed
3335

make.ps1

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,87 @@ switch ($Command.ToLower())
449449
}
450450
}
451451

452+
# pony-doc-tests
453+
if ($TestsToRun -match 'pony-doc-tests')
454+
{
455+
$numTestSuitesRun += 1;
456+
Write-Output "$outDir\ponyc.exe --path $srcDir\tools\lib\ponylang\pony_compiler\ -b pony-doc-tests -o $outDir $srcDir\tools\pony-doc\test"
457+
& $outDir\ponyc.exe --path $srcDir\tools\lib\ponylang\pony_compiler\ -b pony-doc-tests -o $outDir $srcDir\tools\pony-doc\test
458+
if ($LastExitCode -eq 0)
459+
{
460+
try
461+
{
462+
Write-Output "$outDir\pony-doc-tests.exe --sequential"
463+
& $outDir\pony-doc-tests.exe --sequential
464+
$err = $LastExitCode
465+
}
466+
catch
467+
{
468+
$err = -1
469+
}
470+
if ($err -ne 0) { $failedTestSuites += 'pony-doc-tests' }
471+
}
472+
else
473+
{
474+
$failedTestSuites += 'compile pony-doc-tests'
475+
}
476+
}
477+
478+
# pony-lint-tests
479+
if ($TestsToRun -match 'pony-lint-tests')
480+
{
481+
$numTestSuitesRun += 1;
482+
Write-Output "$outDir\ponyc.exe --path $srcDir\tools\lib\ponylang\pony_compiler\ -b pony-lint-tests -o $outDir $srcDir\tools\pony-lint\test"
483+
& $outDir\ponyc.exe --path $srcDir\tools\lib\ponylang\pony_compiler\ -b pony-lint-tests -o $outDir $srcDir\tools\pony-lint\test
484+
if ($LastExitCode -eq 0)
485+
{
486+
$savePonyPath = $env:PONYPATH
487+
$env:PONYPATH = "$srcDir\packages;$savePonyPath"
488+
try
489+
{
490+
Write-Output "$outDir\pony-lint-tests.exe --sequential"
491+
& $outDir\pony-lint-tests.exe --sequential
492+
$err = $LastExitCode
493+
}
494+
catch
495+
{
496+
$err = -1
497+
}
498+
$env:PONYPATH = $savePonyPath
499+
if ($err -ne 0) { $failedTestSuites += 'pony-lint-tests' }
500+
}
501+
else
502+
{
503+
$failedTestSuites += 'compile pony-lint-tests'
504+
}
505+
}
506+
507+
# pony-lsp-tests
508+
if ($TestsToRun -match 'pony-lsp-tests')
509+
{
510+
$numTestSuitesRun += 1;
511+
Write-Output "$outDir\ponyc.exe --path $srcDir\tools\lib\ponylang\peg --path $srcDir\tools\lib\ponylang\pony_compiler\ -b pony-lsp-tests -o $outDir $srcDir\tools"
512+
& $outDir\ponyc.exe --path $srcDir\tools\lib\ponylang\peg --path $srcDir\tools\lib\ponylang\pony_compiler\ -b pony-lsp-tests -o $outDir $srcDir\tools
513+
if ($LastExitCode -eq 0)
514+
{
515+
try
516+
{
517+
Write-Output "$outDir\pony-lsp-tests.exe --sequential"
518+
& $outDir\pony-lsp-tests.exe --sequential
519+
$err = $LastExitCode
520+
}
521+
catch
522+
{
523+
$err = -1
524+
}
525+
if ($err -ne 0) { $failedTestSuites += 'pony-lsp-tests' }
526+
}
527+
else
528+
{
529+
$failedTestSuites += 'compile pony-lsp-tests'
530+
}
531+
}
532+
452533
#
453534
$numTestSuitesFailed = $failedTestSuites.Length
454535
Write-Output "Test suites run: $numTestSuitesRun, num failed: $numTestSuitesFailed"

tools/pony-lint/ignore_matcher.pony

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,18 @@ class ref IgnoreMatcher
126126

127127
let matched =
128128
if pat.anchored then
129-
// Match against path relative to the rule's base directory
129+
// Match against path relative to the rule's base directory,
130+
// normalized to forward slashes for GlobMatch
130131
let rel =
131132
try
132-
Path.rel(base_dir, abs_path)?
133+
let r = Path.rel(base_dir, abs_path)?
134+
ifdef windows then
135+
let s = r.clone()
136+
s.replace("\\", "/")
137+
consume s
138+
else
139+
r
140+
end
133141
else
134142
abs_path
135143
end
@@ -154,9 +162,9 @@ class ref IgnoreMatcher
154162
"""
155163
if abs_path.at(base_dir) then
156164
let blen = base_dir.size()
157-
// Exact match or path continues with /
165+
// Exact match or path continues with a separator
158166
(abs_path.size() == blen)
159-
or (try abs_path(blen)? == '/' else false end)
167+
or (try Path.is_sep(abs_path(blen)?) else false end)
160168
else
161169
false
162170
end

tools/pony-lint/linter.pony

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ class val Linter
457457
else
458458
return
459459
end
460-
let parts = rel.split_by("/")
460+
let parts = rel.split_by(Path.sep())
461461
var current: String val = root
462462
for part in (consume parts).values() do
463463
current = Path.join(current, part)

tools/pony-lsp/_unreachable.pony

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use @fprintf[I32](stream: Pointer[U8] tag, fmt: Pointer[U8] tag, ...)
2+
use @pony_os_stderr[Pointer[U8]]()
3+
use @exit[None](code: I32)
4+
5+
primitive _Unreachable
6+
"""
7+
Crashes the program when a code path that should be unreachable is reached.
8+
9+
Use in `else` blocks of `try` expressions where bounds are guarded by prior
10+
checks and the error path is logically impossible.
11+
"""
12+
fun apply(loc: SourceLoc = __loc): None =>
13+
let url = "https://github.com/ponylang/ponyc/issues"
14+
let fmt = "Unreachable at %s:%zu\nPlease report: " + url + "\n"
15+
@fprintf(
16+
@pony_os_stderr(),
17+
fmt.cstring(),
18+
loc.file().cstring(),
19+
loc.line())
20+
@exit(1)

tools/pony-lsp/test/_diagnostics_tests.pony

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class \nodoc\ iso _DiagnosticTest is UnitTest
4343
Methods.text_document().did_open(),
4444
JsonObject
4545
.update("textDocument", JsonObject
46-
.update("uri", "file://" + Path.join(workspace_dir, "main.pony"))
46+
.update("uri", Uris.from_path(Path.join(workspace_dir, "main.pony")))
4747
.update("languageId", "pony")
4848
.update("version", I64(1))
4949
.update("text", "don't care"))

tools/pony-lsp/test/_workspace_tests.pony

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,28 @@ class \nodoc\ iso _RouterFindTest is UnitTest
2222
let scanner = WorkspaceScanner.create(channel)
2323
let workspaces = scanner.scan(file_auth, this_dir_path)
2424
h.assert_eq[USize](3, workspaces.size())
25-
// main.pony workspace has been found first
26-
var workspace = workspaces(0)?
27-
h.assert_eq[String](folder.path, workspace.folder.path)
28-
29-
// corral workspace
30-
workspace = workspaces(1)?
31-
h.assert_eq[String](folder.join("error_workspace")?.path, workspace.folder.path)
32-
33-
// corral error workspace
34-
workspace = workspaces(2)?
35-
h.assert_eq[String](folder.join("workspace")?.path, workspace.folder.path)
3625

26+
// Verify all expected workspaces were found (order is not guaranteed
27+
// because path.walk uses filesystem enumeration order)
28+
let actual = Array[String]
29+
for ws in workspaces.values() do
30+
actual.push(ws.folder.path)
31+
end
32+
let expected = [as String:
33+
folder.path
34+
folder.join("error_workspace")?.path
35+
folder.join("workspace")?.path
36+
]
37+
h.assert_array_eq_unordered[String](expected, actual)
38+
39+
// Verify router lookup works with any workspace
3740
let router = WorkspaceRouter.create()
3841
let compiler = PonyCompiler("") // dummy, not actually in use
3942
let request_sender = FakeRequestSender
4043
let client = Client.from(JsonObject)
4144

42-
let mgr = WorkspaceManager(workspace, file_auth, channel, request_sender, client, compiler)
45+
let mgr = WorkspaceManager(
46+
workspaces(0)?, file_auth, channel, request_sender, client, compiler)
4347
router.add_workspace(folder, mgr)?
4448

4549
let file_path = folder.join("main.pony")?

tools/pony-lsp/test/utils.pony

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,30 @@ primitive LspMsg
142142
}
143143
""")
144144

145+
fun tag _json_escape(s: String val): String val =>
146+
"""
147+
Escape backslashes for safe embedding in JSON string literals. On
148+
Windows, filesystem paths contain `\\` which must be doubled for JSON.
149+
"""
150+
ifdef windows then
151+
if s.contains("\\") then
152+
let out = s.clone()
153+
out.replace("\\", "\\\\")
154+
consume out
155+
else
156+
s
157+
end
158+
else
159+
s
160+
end
161+
145162
fun tag initialize(
146163
dir: String,
147164
did_change_configuration_dynamic_registration: Bool = true,
148165
supports_configuration: Bool = true
149166
): Array[U8] iso^ =>
167+
let safe_dir = _json_escape(dir)
168+
let dir_uri = Uris.from_path(dir)
150169
this.apply("""
151170
{
152171
"jsonrpc": "2.0",
@@ -159,9 +178,9 @@ primitive LspMsg
159178
"version": "1.88.0"
160179
},
161180
"locale": "en",
162-
"rootPath": """" + dir + """
181+
"rootPath": """" + safe_dir + """
163182
",
164-
"rootUri": "file://""" + dir + """
183+
"rootUri": """" + dir_uri + """
165184
",
166185
"capabilities": {
167186
"workspace": {
@@ -632,7 +651,7 @@ primitive LspMsg
632651
"trace": "verbose",
633652
"workspaceFolders": [
634653
{
635-
"uri": "file://""" + dir + """
654+
"uri": """" + dir_uri + """
636655
",
637656
"name": "workspace"
638657
}

0 commit comments

Comments
 (0)