Skip to content

Commit 6e57861

Browse files
authored
fix(windows): #120 explicit /SUBSYSTEM:CONSOLE for non-UI PE builds (v0.5.179)
Closes #120. The actual fix (gating `/SUBSYSTEM:CONSOLE` vs `/SUBSYSTEM:WINDOWS` on `ctx.needs_ui`) already landed in v0.5.133. This PR extracts the inline ternary into a named `windows_pe_subsystem_flag(needs_ui)` helper with a doc-comment referencing #120, and adds two `windows_link_tests` unit tests (`cli_build_uses_console_subsystem` / `ui_build_uses_windows_subsystem`) that pin the flag choice so a future refactor can't silently re-break it. No behavior change. Verified by the Windows-2022 doc-tests runner in CI. Cloud-authored PR, manually audited and metadata (version bump + CLAUDE.md entry) folded in at merge.
1 parent 12d572b commit 6e57861

3 files changed

Lines changed: 37 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88

99
Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation.
1010

11-
**Current Version:** 0.5.178
11+
**Current Version:** 0.5.179
1212

1313
## TypeScript Parity Status
1414

@@ -147,6 +147,7 @@ First-resolved directory cached in `compile_package_dirs`; subsequent imports re
147147

148148
Keep entries to 1-2 lines max. Full details in CHANGELOG.md.
149149

150+
- **v0.5.179** — Fix #120 via PR #160 (cloud-authored, audited): Windows CLI executables had their PE subsystem set to `WINDOWS` (2) instead of `CONSOLE` (3), so `console.log` output was silently detached. The actual flag selection (`/SUBSYSTEM:CONSOLE` vs `/SUBSYSTEM:WINDOWS` gated on `ctx.needs_ui`) already landed in v0.5.133 as an inline ternary at `compile.rs:6240`-ish; this PR extracts it into a named `windows_pe_subsystem_flag(needs_ui: bool) -> &'static str` helper with a doc-comment pointing at #120, and adds two `windows_link_tests` unit tests (`cli_build_uses_console_subsystem` / `ui_build_uses_windows_subsystem`) that pin the flag choice so a future refactor can't silently re-break it. No behavior change; verified by CI including the `windows-2022` doc-tests runner.
150151
- **v0.5.178** — Fix-forward for v0.5.177 release-packages failure (tests gate red on three distinct regressions exposed by the fresh CI run): (1) **`App({...})` inside docs/examples/ui/** — the Phase 3 anon-class synthesis in v0.5.172 started wrapping closed-shape `{title,width,height,body}` literals into `new __AnonShape_N(...)`, but the `perry/ui: App()` handler at `lower_call.rs:2436` still `let Expr::Object(props) = &args[0] else { bail }`'d, so every UI doc example on macOS/Ubuntu/Windows failed with "App(...) requires a config object literal." Fix: swap the raw Object match for `extract_options_fields(ctx, &args[0])` (the same helper perry/thread's spawn options already use for both raw-Object and AnonShape-New paths). 19/27 doc-tests → 27/27. (2) **`just_factory` stack overflow in repro_test.rs** — a test added alongside `class_extends_plus_top_level_call_overflows` in v0.5.167 era to narrow a pre-existing compiler stack overflow (top-level `const x = fn()` + factory returning nested object literal). The sibling test was already `#[ignore]`d; `just_factory` was not, so cargo-test aborted with SIGABRT. Marked `#[ignore]` with the same rationale; real fix tracked separately. (3) **`new Set([...])` without explicit `<number>` type args** — `refine_type_from_init` in `codegen/type_analysis.rs:80` was emitting `HirType::Named("Set")` for `Expr::SetNewFromArray`, but `is_set_expr` (same module, :564) only matches `HirType::Generic { base: "Set", .. }`. So `const s = new Set([1,2,3]); s.has(1)` silently missed the Set fast path and returned `undefined` instead of `true`. Mirror bug in `perry-hir/src/lower_types.rs`'s `ast::Expr::New` arm (Phase 4 inference) which also fell to `Type::Named` without explicit type_args — fixed by routing Map/Set/WeakMap/WeakSet/Array/Promise through `Type::Generic { base, type_args: [] }` when no args are given (intrinsic generics can't be Named). Parity suite: `test_edge_map_set`'s Set.has() calls now hit the fast path; the test's specific setA.forEach-over-setB.has pattern passes for every shape except one path-dependent environmental flake (same file MD5 compiles + runs correctly from `/tmp/` but produces wrong intersection from project root — different bug, added to `known_failures.json` with the repro noted).
151152
- **v0.5.177** — Fix #142 via PR #148: `Math.tan`/`asin`/`acos`/`atan`/`atan2` were lowering to silent identity functions. The five arms at `crates/perry-codegen/src/expr.rs:5417-5424` had an old "no runtime wrappers yet, no LLVM intrinsics for these" comment and fell through to `lower_expr(ctx, o)` — returning the argument unchanged with no diagnostic. The runtime functions (`js_math_tan/asin/acos/atan/atan2` in `perry-runtime/src/math.rs:46-62`, each a thin `f64::tan()` etc.) and the extern declarations (`runtime_decls.rs:1488-1499`) had actually been in place for a while; the codegen wiring was just missing. Replaced the fall-through with five `ctx.block().call(DOUBLE, "js_math_*", ...)` arms matching the shape of the already-working `sinh/cosh/tanh` arms three lines above. `atan2(y, x)` evaluates y first then x (JS left-to-right argument order) and passes both to `js_math_atan2`. Verified against Node `--experimental-strip-types`: `Math.tan(1) = 1.557…` (was `1`), `Math.atan2(0,-1) = π` (was `-1`), `Math.asin(1) = π/2`, `Math.acos(1) = 0`, `Math.atan(1) = π/4` — all within last-digit libm rounding. Merged via conflict-resolved cherry-pick since the PR branch was cut at v0.5.164 and `main` had advanced to v0.5.176.
152153
- **v0.5.176** — Fix #158: `Map`/`Set` now treat `-0` and `+0` as the same key (SameValueZero, 23.1.3.9 / 23.2.3.1). Added a `normalize_zero(v: f64) -> f64` helper in `crates/perry-runtime/src/map.rs` and `set.rs` that rewrites `-0` bits (`0x8000_0000_0000_0000`) to `+0` via an `v == 0.0` IEEE check — safe against NaN-boxed tagged values because any tag in the upper 16 bits makes the f64 a NaN and `NaN == 0.0` is false. Wired into `js_map_set` / `_get` / `_has` / `_delete` and `js_set_add` / `_has` / `_delete` so both insert and lookup paths normalize. Previously `numMap.set(0,"a"); numMap.set(-0,"b")` yielded size=2 with `get(0)` returning "a"; now yields size=1 with `get(0)`="b" matching Node's `--experimental-strip-types`.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ opt-level = "s" # Optimize for size in stdlib
104104
opt-level = 3
105105

106106
[workspace.package]
107-
version = "0.5.178"
107+
version = "0.5.179"
108108
edition = "2021"
109109
license = "MIT"
110110
repository = "https://github.com/PerryTS/perry"

crates/perry/src/commands/compile.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,17 @@ fn find_msvc_link_exe() -> Option<PathBuf> {
11641164
find_llvm_tool("lld-link")
11651165
}
11661166

1167+
/// Returns the `/SUBSYSTEM:…` flag for MSVC `link.exe` / `lld-link`.
1168+
///
1169+
/// CLI programs must use `CONSOLE` (3) so the OS loader attaches stdin/stdout/stderr
1170+
/// before `main()` runs. GUI programs use `WINDOWS` (2) to suppress the console
1171+
/// window that would otherwise flash alongside the app window. Passing neither
1172+
/// flag lets the linker pick a default, which historically resolved to `WINDOWS`
1173+
/// for Perry builds and silently discarded all `console.log` output (issue #120).
1174+
fn windows_pe_subsystem_flag(needs_ui: bool) -> &'static str {
1175+
if needs_ui { "/SUBSYSTEM:WINDOWS" } else { "/SUBSYSTEM:CONSOLE" }
1176+
}
1177+
11671178
/// Find MSVC library search paths (MSVC CRT, Windows SDK um, Windows SDK ucrt).
11681179
/// Returns a semicolon-separated string suitable for the LIB environment variable.
11691180
#[cfg(target_os = "windows")]
@@ -6237,14 +6248,10 @@ pub fn run_with_parse_cache(
62376248
})
62386249
};
62396250
let mut c = Command::new(linker);
6240-
// CONSOLE for CLI programs so the loader attaches stdin/stdout/stderr
6241-
// before main() runs — otherwise println!() in js_console_log writes
6242-
// to a detached handle and nothing appears in the terminal (#108).
6243-
// WINDOWS for UI programs so no console flashes alongside the window.
6244-
// /ENTRY:mainCRTStartup works for both: Perry emits `int main()` and
6245-
// the MSVC CRT invokes it regardless of subsystem.
6246-
let subsystem = if ctx.needs_ui { "/SUBSYSTEM:WINDOWS" } else { "/SUBSYSTEM:CONSOLE" };
6247-
c.arg(subsystem)
6251+
// /ENTRY:mainCRTStartup works for both subsystems: Perry emits
6252+
// `int main()` and the MSVC CRT invokes it regardless of subsystem.
6253+
// See windows_pe_subsystem_flag() for subsystem selection rationale.
6254+
c.arg(windows_pe_subsystem_flag(ctx.needs_ui))
62486255
.arg("/ENTRY:mainCRTStartup")
62496256
.arg("/NOLOGO")
62506257
// Perry generates large init functions for TS modules (one function
@@ -8403,3 +8410,22 @@ mod object_cache_tests {
84038410
assert_eq!(a.lookup(0x777).as_deref(), Some(b"from-a".as_ref()));
84048411
}
84058412
}
8413+
8414+
#[cfg(test)]
8415+
mod windows_link_tests {
8416+
use super::windows_pe_subsystem_flag;
8417+
8418+
// Regression guard for issue #120: without an explicit subsystem flag the
8419+
// MSVC linker historically defaulted to WINDOWS (2), silently detaching
8420+
// stdout/stderr so console.log output never reached the terminal.
8421+
8422+
#[test]
8423+
fn cli_build_uses_console_subsystem() {
8424+
assert_eq!(windows_pe_subsystem_flag(false), "/SUBSYSTEM:CONSOLE");
8425+
}
8426+
8427+
#[test]
8428+
fn ui_build_uses_windows_subsystem() {
8429+
assert_eq!(windows_pe_subsystem_flag(true), "/SUBSYSTEM:WINDOWS");
8430+
}
8431+
}

0 commit comments

Comments
 (0)