Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions crates/perry-codegen-js/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,13 @@ impl JsEmitter {
self.emit_expr(b);
self.output.push(')');
}
Expr::PathWin32Join(a, b) => {
self.output.push_str("__perry.path.win32.join(");
self.emit_expr(a);
self.output.push_str(", ");
self.emit_expr(b);
self.output.push(')');
}
Expr::PathDirname(p) => {
self.output.push_str("__perry.path.dirname(");
self.emit_expr(p);
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-codegen-wasm/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8560,6 +8560,12 @@ impl<'a> FuncEmitCtx<'a> {
self.emit_store_arg(func, 1, b);
self.emit_memcall(func, "path_join", 2);
}
Expr::PathWin32Join(a, b) => {
self.emit_frame_begin(func, 2);
self.emit_store_arg(func, 0, a);
self.emit_store_arg(func, 1, b);
self.emit_memcall(func, "path_win32_join", 2);
}
Expr::PathDirname(p) => {
self.emit_frame_begin(func, 1);
self.emit_store_arg(func, 0, p);
Expand Down
11 changes: 9 additions & 2 deletions crates/perry-codegen/src/collectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,10 @@ pub(crate) fn collect_ref_ids_in_expr(e: &perry_hir::Expr, out: &mut HashSet<u32
walk(v, out);
}
}
Expr::MathPow(a, b) | Expr::PathJoin(a, b) | Expr::PathRelative(a, b) => {
Expr::MathPow(a, b)
| Expr::PathJoin(a, b)
| Expr::PathRelative(a, b)
| Expr::PathWin32Join(a, b) => {
walk(a, out);
walk(b, out);
}
Expand Down Expand Up @@ -3308,7 +3311,10 @@ fn collect_localset_ids_in_expr_filtered(
walk(v, out);
}
}
Expr::MathPow(a, b) | Expr::PathJoin(a, b) | Expr::PathRelative(a, b) => {
Expr::MathPow(a, b)
| Expr::PathJoin(a, b)
| Expr::PathRelative(a, b)
| Expr::PathWin32Join(a, b) => {
walk(a, out);
walk(b, out);
}
Expand Down Expand Up @@ -4849,6 +4855,7 @@ fn check_escapes_in_expr(
}
Expr::MathPow(a, b)
| Expr::PathJoin(a, b)
| Expr::PathWin32Join(a, b)
| Expr::ObjectIs(a, b)
| Expr::ObjectHasOwn(a, b) => {
check_escapes_in_expr(a, candidates, classes, escaped);
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-codegen/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6507,6 +6507,24 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
Ok(nanbox_string_inline(blk, &result))
}

// -------- path.win32.join(a, b) -> string (issue #810) --------
// Windows-style join with `\` separator, regardless of host
// platform. Multi-arg path.win32.join lowers to chained
// PathWin32Join in the HIR.
Expr::PathWin32Join(a, b) => {
let a_box = lower_expr(ctx, a)?;
let b_box = lower_expr(ctx, b)?;
let blk = ctx.block();
let a_handle = unbox_to_i64(blk, &a_box);
let b_handle = unbox_to_i64(blk, &b_box);
let result = blk.call(
I64,
"js_path_win32_join",
&[(I64, &a_handle), (I64, &b_handle)],
);
Ok(nanbox_string_inline(blk, &result))
}

// -------- queueMicrotask(fn) / process.nextTick(fn) stubs --------
// Real microtask scheduling needs the runtime's queue. For
// now we lower the callback for side effects (it might be a
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_object_values", I64, &[I64]);
module.declare_function("js_object_entries", I64, &[I64]);
module.declare_function("js_path_join", I64, &[I64, I64]);
module.declare_function("js_path_win32_join", I64, &[I64, I64]);
module.declare_function("js_path_dirname", I64, &[I64]);
module.declare_function("js_path_relative", I64, &[I64, I64]);
module.declare_function("js_path_to_namespaced_path", I64, &[I64]);
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-codegen/src/type_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ pub(crate) fn is_definitely_string_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool {
| Expr::PathNormalize(_)
| Expr::PathToNamespacedPath(_)
| Expr::PathResolveJoin(..)
| Expr::PathWin32Join(..)
| Expr::ProcessVersion
| Expr::ProcessCwd
| Expr::OsArch
Expand Down Expand Up @@ -875,7 +876,8 @@ pub(crate) fn is_string_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool {
| Expr::PathResolve(_)
| Expr::PathNormalize(_)
| Expr::PathToNamespacedPath(_)
| Expr::PathResolveJoin(..) => true,
| Expr::PathResolveJoin(..)
| Expr::PathWin32Join(..) => true,
// String.fromCodePoint(...) / String.fromCharCode(...) / str.at(i)
// / RegExp.source|flags — all produce string handles.
Expr::StringFromCodePoint(_)
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-hir/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,10 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec<Local
collect_assigned_locals_expr(h, assigned);
}
// Path operations
Expr::PathJoin(a, b) | Expr::PathMatchesGlob(a, b) | Expr::PathResolveJoin(a, b) => {
Expr::PathJoin(a, b)
| Expr::PathMatchesGlob(a, b)
| Expr::PathResolveJoin(a, b)
| Expr::PathWin32Join(a, b) => {
collect_assigned_locals_expr(a, assigned);
collect_assigned_locals_expr(b, assigned);
}
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,7 @@ pub enum Expr {
PathToNamespacedPath(Box<Expr>), // path.toNamespacedPath(path) -> string (POSIX: no-op)
PathMatchesGlob(Box<Expr>, Box<Expr>), // path.matchesGlob(path, pattern) -> boolean
PathResolveJoin(Box<Expr>, Box<Expr>), // internal: join with reset-on-absolute (multi-arg resolve)
PathWin32Join(Box<Expr>, Box<Expr>), // path.win32.join(a, b) -> string (issue #810)

// WeakRef and FinalizationRegistry
WeakRefNew(Box<Expr>), // new WeakRef(obj) -> WeakRef
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/js_transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ fn transform_expr(
Expr::FsReadFileSync(e) | Expr::FsExistsSync(e) | Expr::FsMkdirSync(e) | Expr::FsUnlinkSync(e) => {
transform_expr(e, js_imports, extern_func_to_js, local_name_to_js, tracker);
}
Expr::FsWriteFileSync(a, b) | Expr::FsAppendFileSync(a, b) | Expr::PathJoin(a, b) | Expr::PathMatchesGlob(a, b) | Expr::PathResolveJoin(a, b) | Expr::MathPow(a, b) | Expr::MathImul(a, b) => {
Expr::FsWriteFileSync(a, b) | Expr::FsAppendFileSync(a, b) | Expr::PathJoin(a, b) | Expr::PathMatchesGlob(a, b) | Expr::PathResolveJoin(a, b) | Expr::PathWin32Join(a, b) | Expr::MathPow(a, b) | Expr::MathImul(a, b) => {
transform_expr(a, js_imports, extern_func_to_js, local_name_to_js, tracker);
transform_expr(b, js_imports, extern_func_to_js, local_name_to_js, tracker);
}
Expand Down
166 changes: 166 additions & 0 deletions crates/perry-hir/src/lower/expr_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,172 @@ pub(super) fn lower_call(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Res
}
}

// `path.posix.<method>(args)` / `path.win32.<method>(args)` —
// sub-namespace dispatch (issue #810). Without this arm the
// generic mod.X.Y() block below skips the call (path.posix /
// path.win32 are in its sub-namespace exclusion list to keep
// them off the strict-API gate) and the call falls through
// to the receiver-less dispatch, returning undefined.
//
// - `path.posix.X` routes to the existing Expr::PathX variant.
// The runtime js_path_* functions use POSIX (`/`) semantics,
// so this is a direct shape rewrite.
// - `path.win32.join` routes to a dedicated Expr::PathWin32Join
// so the result uses `\` separators. Other path.win32.*
// methods are intentionally unwired here — falling through
// to a POSIX impl would silently mis-handle drive letters
// and `\` segments. Track those in a follow-up.
if let ast::Expr::Member(outer_member) = expr.as_ref() {
if let ast::Expr::Member(inner_member) = outer_member.obj.as_ref() {
if let ast::Expr::Ident(root_ident) = inner_member.obj.as_ref() {
let root_name = root_ident.sym.as_ref();
let is_path_root = root_name == "path"
|| ctx.lookup_builtin_module_alias(root_name) == Some("path")
|| ctx
.lookup_native_module(root_name)
.map(|(m, _)| m == "path")
.unwrap_or(false);
if is_path_root {
if let (
ast::MemberProp::Ident(sub_prop),
ast::MemberProp::Ident(method_prop),
) = (&inner_member.prop, &outer_member.prop)
{
let sub = sub_prop.sym.as_ref();
let method = method_prop.sym.as_ref();
if sub == "posix" || sub == "win32" {
// path.<sub>.join(...)
if method == "join" {
if args.is_empty() {
return Ok(Expr::String(".".to_string()));
}
if sub == "win32" {
let mut iter = args.into_iter();
let mut result = iter.next().unwrap();
for next_arg in iter {
result = Expr::PathWin32Join(
Box::new(result),
Box::new(next_arg),
);
}
return Ok(result);
} else {
// posix.join → existing PathJoin
if args.len() == 1 {
return Ok(Expr::PathNormalize(Box::new(
args.into_iter().next().unwrap(),
)));
}
let mut iter = args.into_iter();
let mut result = iter.next().unwrap();
for next_arg in iter {
result = Expr::PathJoin(
Box::new(result),
Box::new(next_arg),
);
}
return Ok(result);
}
}

// The remaining methods route to the
// existing POSIX Expr::Path* variants
// only for the `posix` sub-namespace.
// For `win32` we deliberately fall
// through to the receiver-less path
// (returns undefined) rather than
// give a wrong POSIX answer.
if sub == "posix" {
match method {
"dirname" if !args.is_empty() => {
return Ok(Expr::PathDirname(Box::new(
args.into_iter().next().unwrap(),
)));
}
"basename" if args.len() >= 2 => {
let mut it = args.into_iter();
let p = it.next().unwrap();
let e = it.next().unwrap();
return Ok(Expr::PathBasenameExt(
Box::new(p),
Box::new(e),
));
}
"basename" if !args.is_empty() => {
return Ok(Expr::PathBasename(Box::new(
args.into_iter().next().unwrap(),
)));
}
"extname" if !args.is_empty() => {
return Ok(Expr::PathExtname(Box::new(
args.into_iter().next().unwrap(),
)));
}
"isAbsolute" if !args.is_empty() => {
return Ok(Expr::PathIsAbsolute(Box::new(
args.into_iter().next().unwrap(),
)));
}
"normalize" if !args.is_empty() => {
return Ok(Expr::PathNormalize(Box::new(
args.into_iter().next().unwrap(),
)));
}
"parse" if !args.is_empty() => {
return Ok(Expr::PathParse(Box::new(
args.into_iter().next().unwrap(),
)));
}
"format" if !args.is_empty() => {
return Ok(Expr::PathFormat(Box::new(
args.into_iter().next().unwrap(),
)));
}
"toNamespacedPath" if !args.is_empty() => {
return Ok(Expr::PathToNamespacedPath(Box::new(
args.into_iter().next().unwrap(),
)));
}
"relative" if args.len() >= 2 => {
let mut it = args.into_iter();
let from = it.next().unwrap();
let to = it.next().unwrap();
return Ok(Expr::PathRelative(
Box::new(from),
Box::new(to),
));
}
"resolve" if !args.is_empty() => {
let mut it = args.into_iter();
let first = it.next().unwrap();
let mut joined = first;
for next_arg in it {
joined = Expr::PathResolveJoin(
Box::new(joined),
Box::new(next_arg),
);
}
return Ok(Expr::PathResolve(Box::new(joined)));
}
"matchesGlob" if args.len() >= 2 => {
let mut it = args.into_iter();
let p = it.next().unwrap();
let pat = it.next().unwrap();
return Ok(Expr::PathMatchesGlob(
Box::new(p),
Box::new(pat),
));
}
_ => {}
}
}
}
}
}
}
}
}

// Check for module.Class.staticMethod() pattern (e.g.,
// ethers.Wallet.createRandom()). Modelled after the
// process.hrtime.bigint() handler above.
Expand Down
14 changes: 12 additions & 2 deletions crates/perry-hir/src/monomorph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,10 @@ fn substitute_expr(expr: &Expr, substitutions: &HashMap<String, Type>) -> Expr {
Box::new(substitute_expr(a, substitutions)),
Box::new(substitute_expr(b, substitutions)),
),
Expr::PathWin32Join(a, b) => Expr::PathWin32Join(
Box::new(substitute_expr(a, substitutions)),
Box::new(substitute_expr(b, substitutions)),
),
Expr::PathDirname(path) => {
Expr::PathDirname(Box::new(substitute_expr(path, substitutions)))
}
Expand Down Expand Up @@ -2416,7 +2420,10 @@ fn collect_instantiations_in_expr(
collect_instantiations_in_expr(path, ctx, module, idx);
collect_instantiations_in_expr(content, ctx, module, idx);
}
Expr::PathJoin(a, b) | Expr::PathMatchesGlob(a, b) | Expr::PathResolveJoin(a, b) => {
Expr::PathJoin(a, b)
| Expr::PathMatchesGlob(a, b)
| Expr::PathResolveJoin(a, b)
| Expr::PathWin32Join(a, b) => {
collect_instantiations_in_expr(a, ctx, module, idx);
collect_instantiations_in_expr(b, ctx, module, idx);
}
Expand Down Expand Up @@ -2935,7 +2942,10 @@ fn update_call_sites_in_expr(
update_call_sites_in_expr(path, ctx, lookup);
update_call_sites_in_expr(content, ctx, lookup);
}
Expr::PathJoin(a, b) | Expr::PathMatchesGlob(a, b) | Expr::PathResolveJoin(a, b) => {
Expr::PathJoin(a, b)
| Expr::PathMatchesGlob(a, b)
| Expr::PathResolveJoin(a, b)
| Expr::PathWin32Join(a, b) => {
update_call_sites_in_expr(a, ctx, lookup);
update_call_sites_in_expr(b, ctx, lookup);
}
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-hir/src/stable_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1733,6 +1733,11 @@ impl SH for Expr {
a.as_ref().hash(h);
b.as_ref().hash(h);
}
Expr::PathWin32Join(a, b) => {
tag(h, 462);
a.as_ref().hash(h);
b.as_ref().hash(h);
}
Expr::PathDirname(e) => {
tag(h, 91);
e.as_ref().hash(h);
Expand Down
Loading
Loading