diff --git a/changelog.d/10222-namespace-classes.md b/changelog.d/10222-namespace-classes.md new file mode 100644 index 0000000000..05adfca6c7 --- /dev/null +++ b/changelog.d/10222-namespace-classes.md @@ -0,0 +1 @@ +Fix incomplete lowering of classes declared inside TypeScript namespaces. Exported classes are published as namespace members after evaluating dynamic heritage, computed names, static fields and blocks, and legacy decorators in declaration order. Namespace-local class names are qualified internally so nested namespaces and repeated names remain distinct, while private classes stay available to sibling functions. This fixes missing namespace constructors, uninitialized static fields, and inherited static calls such as Effect's `Context.Service` factory used by OpenCode (#10107). diff --git a/crates/perry-hir/src/lower/lower_expr/arm_ident.rs b/crates/perry-hir/src/lower/lower_expr/arm_ident.rs index 381f668fa8..3485153715 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_ident.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_ident.rs @@ -166,6 +166,22 @@ pub(crate) fn lower_ident_expr(ctx: &mut LoweringContext, ident: &ast::Ident) -> } Ok(Expr::LocalGet(id)) } else if let Some(id) = ctx.lookup_func(&name) { + // #10222: inside a namespace body an EXPORTED namespace function is + // emitted as a static method of the namespace class, not as a module + // function. Calls are redirected to `StaticMethodCall` (expr_call), but + // a VALUE reference (`const f = g`, `call(g)`, `Effect.gen(g)`) still + // lowered to `FuncRef(id)` — a closure over a function that has no + // module-function body — and calling it returned garbage. Read the + // published namespace member instead, exactly as `NS.g` resolves from + // outside the namespace; this also keeps `g === NS.g`. + if let Some(ref ns_name) = ctx.current_namespace { + if ctx.has_static_method(ns_name, &name) { + return Ok(Expr::IndexGet { + object: Box::new(Expr::ClassRef(ns_name.clone())), + index: Box::new(Expr::String(name)), + }); + } + } Ok(Expr::FuncRef(id)) } else if ctx.lookup_native_module(&name).is_some() { Ok(native_module_binding_value(ctx, &name)) diff --git a/crates/perry-hir/src/lower/module_decl/namespace.rs b/crates/perry-hir/src/lower/module_decl/namespace.rs index a82a378cac..edd70a2790 100644 --- a/crates/perry-hir/src/lower/module_decl/namespace.rs +++ b/crates/perry-hir/src/lower/module_decl/namespace.rs @@ -24,6 +24,34 @@ fn nested_namespace_name(ts_module: &ast::TsModuleDecl) -> Option { } } +/// Publish a namespace export at its declaration position. An initializer in +/// the field metadata prevents codegen's uninitialized-class-field pass from +/// creating every property before the namespace body executes. The inline set +/// below is authoritative, so the late initializer fallback skips this field. +fn publish_namespace_member( + module: &mut Module, + fields: &mut Vec, + ns_name: &str, + name: String, + value: Expr, + is_readonly: bool, +) { + fields.push(crate::ir::ClassField { + name: name.clone(), + key_expr: None, + ty: Type::Any, + init: Some(value.clone()), + is_private: false, + is_readonly, + decorators: Vec::new(), + }); + module.init.push(Stmt::Expr(Expr::StaticFieldSet { + class_name: ns_name.to_string(), + field_name: name, + value: Box::new(value), + })); +} + /// #5130: lower a namespace nested inside another (`namespace Outer { export /// namespace Inner { ... } }`). The inner namespace becomes its own synthetic /// class registered under the qualified name `Outer.Inner`, and the outer @@ -50,20 +78,14 @@ fn lower_nested_namespace( // Surface the inner namespace as a static field of the outer one, set to a // ClassRef to the inner class. Mirrors the const-member wiring above. - ns_static_fields.push(crate::ir::ClassField { - name: inner_name.clone(), - key_expr: None, - ty: Type::Any, - init: None, - is_private: false, - is_readonly: true, - decorators: Vec::new(), - }); - module.init.push(Stmt::Expr(Expr::StaticFieldSet { - class_name: outer_ns_name.to_string(), - field_name: inner_name, - value: Box::new(Expr::ClassRef(qualified)), - })); + publish_namespace_member( + module, + ns_static_fields, + outer_ns_name, + inner_name, + Expr::ClassRef(qualified), + true, + ); Ok(()) } @@ -124,7 +146,7 @@ pub(crate) fn lower_namespace_as_class( // `G.Nested` resolves to the inner namespace and `G.Nested.value` / // `G.Nested.f()` read its statics. Registered as static fields up-front so // `has_static_field` routes `G.Nested` to `StaticFieldGet`. - let mut nested_ns_names: Vec = Vec::new(); + let mut ns_static_names: Vec = Vec::new(); // Namespace `export const` members surfaced as static fields so `Ns.member` // resolves CROSS-MODULE (the per-module `namespace_vars` local is invisible // to importers; only namespace FUNCTIONS — lowered as static methods — @@ -134,6 +156,36 @@ pub(crate) fn lower_namespace_as_class( // `util` namespace (`util.objectKeys`, …) is imported this way. let mut ns_static_fields: Vec = Vec::new(); + // Namespace classes have lexical names, even though their definitions live + // in the module's class table. Qualify the registration keys so unrelated + // namespaces can each declare (for example) Service, and pre-register them + // before lowering sibling functions that reference a later declaration. + let saved_class_renames = ctx.class_renames.clone(); + let mut saved_class_bindings = Vec::new(); + for item in items { + let decl = match item { + ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export)) => &export.decl, + ast::ModuleItem::Stmt(ast::Stmt::Decl(decl)) => decl, + _ => continue, + }; + if let ast::Decl::Class(class_decl) = decl { + let name = class_decl.ident.sym.to_string(); + let qualified = format!("{ns_name}.{name}"); + if ctx.lookup_class(&qualified).is_none() { + let id = ctx.fresh_class(); + ctx.register_class(qualified.clone(), id); + } + // Binding probes such as `typeof C` look up the source spelling + // before lowering the value through `class_renames`. Give them a + // scope-local alias to the same class-table entry as well. + let index = ctx.classes_index[&qualified]; + saved_class_bindings + .push((name.clone(), ctx.classes_index.insert(name.clone(), index))); + ctx.class_renames + .insert(name, (qualified, class_decl.span().lo.0)); + } + } + // First pass: collect exported function names, pre-register all functions and variables // (so namespace members can reference each other regardless of declaration order) for item in items { @@ -164,10 +216,13 @@ pub(crate) fn lower_namespace_as_class( } } } + ast::Decl::Class(class_decl) => { + ns_static_names.push(class_decl.ident.sym.to_string()); + } // #5130: nested `export namespace Inner { ... }`. ast::Decl::TsModule(ts_module) if !ts_module.declare => { if let Some(name) = nested_namespace_name(ts_module) { - nested_ns_names.push(name); + ns_static_names.push(name); } } _ => {} @@ -178,7 +233,7 @@ pub(crate) fn lower_namespace_as_class( if !ts_module.declare => { if let Some(name) = nested_namespace_name(ts_module) { - nested_ns_names.push(name); + ns_static_names.push(name); } } // Pre-register non-exported functions (hoisted like JS) @@ -220,7 +275,7 @@ pub(crate) fn lower_namespace_as_class( // resolves via `has_static_field` → `StaticFieldGet` (#5130). ctx.register_class_statics( ns_name.to_string(), - nested_ns_names.clone(), + ns_static_names, static_method_names.clone(), ); @@ -268,7 +323,22 @@ pub(crate) fn lower_namespace_as_class( class.to_string(), )); } + // Keep direct static-method dispatch, and also publish + // its function value as an enumerable namespace member + // alongside classes and variables in source order. + let name = func.name.clone(); static_methods.push(func); + publish_namespace_member( + module, + &mut ns_static_fields, + ns_name, + name.clone(), + Expr::IndexGet { + object: Box::new(Expr::ClassRef(ns_name.to_string())), + index: Box::new(Expr::String(name)), + }, + false, + ); } ast::Decl::Var(var_decl) => { // Lower exported namespace variables as module-level locals @@ -334,24 +404,16 @@ pub(crate) fn lower_namespace_as_class( // Surface as a static field of the namespace class // and copy the const's value into it (after the Let // above), so `Ns.member` resolves cross-module via - // the static-field global. The field carries no - // initializer of its own — the value is set once, - // here, from the already-evaluated local. + // the static-field global without re-evaluation. if is_exported { - ns_static_fields.push(crate::ir::ClassField { - name: name.clone(), - key_expr: None, - ty: Type::Any, - init: None, - is_private: false, - is_readonly: !mutable, - decorators: Vec::new(), - }); - module.init.push(Stmt::Expr(Expr::StaticFieldSet { - class_name: ns_name.to_string(), - field_name: name.clone(), - value: Box::new(Expr::LocalGet(id)), - })); + publish_namespace_member( + module, + &mut ns_static_fields, + ns_name, + name.clone(), + Expr::LocalGet(id), + !mutable, + ); } // Export the variable for cross-module access if is_exported { @@ -366,7 +428,51 @@ pub(crate) fn lower_namespace_as_class( } ast::Decl::Class(class_decl) => { let class = lower_class_decl(ctx, class_decl, is_exported)?; + let class_name = class.name.clone(); + // A declaration evaluates heritage and computed keys + // before static fields/blocks and legacy decorators, + // just like the module-level class declaration path. + if let Some(extends_expr) = &class.extends_expr { + module + .init + .push(Stmt::Expr(Expr::RegisterClassParentDynamic { + class_name: class_name.clone(), + parent_expr: extends_expr.clone(), + })); + } + let (computed_name_evaluations, _) = + crate::lower_decl::prepare_ordered_class_computed_names( + &class_decl.class.body, + &class, + &class_name, + ); + module + .init + .extend(computed_name_evaluations.into_iter().map(Stmt::Expr)); + module.init.extend( + crate::lower_decl::build_interleaved_static_init_stmts_after_computed_names( + &class_decl.class.body, + &class_name, + &class.fields, + &class.static_fields, + &class.static_methods, + ), + ); + append_legacy_decorator_init_for_class(ctx, &mut module.init, &class); push_class_dedup(module, class); + + // `export` here publishes on the namespace regardless + // of whether the namespace itself is a module export. + // Append the field at its source position to preserve + // enumeration order alongside exported variables. + publish_namespace_member( + module, + &mut ns_static_fields, + ns_name, + class_decl.ident.sym.to_string(), + Expr::ClassRef(class_name), + false, + ); } // #5130: nested `export namespace Inner { ... }`. ast::Decl::TsModule(ts_module) => { @@ -387,6 +493,14 @@ pub(crate) fn lower_namespace_as_class( // Restore previous namespace context ctx.current_namespace = prev_namespace; + ctx.class_renames = saved_class_renames; + for (name, previous) in saved_class_bindings.into_iter().rev() { + if let Some(index) = previous { + ctx.classes_index.insert(name, index); + } else { + ctx.classes_index.remove(&name); + } + } Ok(Class { id: class_id, diff --git a/crates/perry-hir/tests/namespace_classes.rs b/crates/perry-hir/tests/namespace_classes.rs new file mode 100644 index 0000000000..9fa8d20b3f --- /dev/null +++ b/crates/perry-hir/tests/namespace_classes.rs @@ -0,0 +1,114 @@ +//! Namespace class declarations need the same evaluation steps as module classes. + +use perry_diagnostics::SourceCache; +use perry_hir::{lower_module, Expr, Module, Stmt}; +use perry_parser::parse_typescript_with_cache; + +fn lower(source: &str) -> Module { + let mut cache = SourceCache::new(); + let parsed = parse_typescript_with_cache(source, "test.ts", &mut cache).expect("parse"); + lower_module(&parsed.module, "test", "test.ts").expect("lower") +} + +#[test] +fn namespace_class_registers_parent_before_initializers_and_publication() { + let module = lower( + r#" + function base() { return class {} } + export namespace N { + export const before = 1; + export class C extends base() { static value = 7 } + export const after = 2; + } + "#, + ); + let namespace = module.classes.iter().find(|c| c.name == "N").unwrap(); + assert_eq!( + namespace + .static_fields + .iter() + .map(|f| f.name.as_str()) + .collect::>(), + ["before", "C", "after"] + ); + let parent = module.init.iter().position(|s| matches!(s, + Stmt::Expr(Expr::RegisterClassParentDynamic { class_name, .. }) if class_name == "N.C" + )).expect("dynamic parent registration"); + let initializer = module + .init + .iter() + .position(|s| { + matches!(s, + Stmt::Expr(Expr::StaticFieldSet { class_name, field_name, .. }) + if class_name == "N.C" && field_name == "value" + ) + }) + .expect("static initializer"); + let publication = module.init.iter().position(|s| matches!(s, + Stmt::Expr(Expr::StaticFieldSet { class_name, field_name, value }) + if class_name == "N" && field_name == "C" && matches!(value.as_ref(), Expr::ClassRef(n) if n == "N.C") + )).expect("namespace publication"); + assert!(parent < initializer && initializer < publication); +} + +#[test] +fn namespace_class_names_are_lexical_and_private_classes_are_not_published() { + let module = lower( + r#" + class C {} + namespace N { + export class C {} + class Hidden { static value = 3 } + export namespace Inner { export class C {} } + } + namespace Other { export class C {} } + "#, + ); + for name in ["C", "N.C", "N.Hidden", "N.Inner.C", "Other.C"] { + assert_eq!( + module.classes.iter().filter(|c| c.name == name).count(), + 1, + "{name}" + ); + } + let namespace = module.classes.iter().find(|c| c.name == "N").unwrap(); + assert_eq!( + namespace + .static_fields + .iter() + .map(|f| f.name.as_str()) + .collect::>(), + ["C", "Inner"] + ); + assert!(module.init.iter().any(|s| matches!(s, + Stmt::Expr(Expr::StaticFieldSet { class_name, field_name, .. }) + if class_name == "N.Hidden" && field_name == "value" + ))); +} + +#[test] +fn namespace_class_value_resolves_inside_a_sibling_arrow() { + let module = lower( + r#" + export namespace N { + export const inside = () => C; + export class C {} + } + "#, + ); + let body = module + .init + .iter() + .find_map(|stmt| match stmt { + Stmt::Let { + name, + init: Some(Expr::Closure { body, .. }), + .. + } if name == "inside" => Some(body), + _ => None, + }) + .expect("sibling arrow"); + assert!(body.iter().any(|stmt| matches!(stmt, + Stmt::Return(Some(Expr::ClassRef(name))) if name == "N.C" + ))); +} diff --git a/crates/perry/tests/namespace_classes.rs b/crates/perry/tests/namespace_classes.rs new file mode 100644 index 0000000000..cd6d4cbf79 --- /dev/null +++ b/crates/perry/tests/namespace_classes.rs @@ -0,0 +1,197 @@ +//! #10222: namespace classes must evaluate and publish like class declarations. +//! Expected output is checked against Bun (namespaces require TS transforms). + +use std::path::PathBuf; +use std::process::Command; + +fn compile_and_run(source: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, source).expect("write entry"); + std::fs::write( + dir.path().join("tsconfig.json"), + r#"{"compilerOptions":{"experimentalDecorators":true}}"#, + ) + .expect("write tsconfig"); + let perry = PathBuf::from(env!("CARGO_BIN_EXE_perry")); + let runtime_dir = std::env::var_os("PERRY_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| perry.parent().expect("binary directory").to_path_buf()); + let compile = Command::new(&perry) + .current_dir(dir.path()) + .env("PERRY_NO_AUTO_OPTIMIZE", "1") + .env("PERRY_RUNTIME_DIR", runtime_dir) + .args(["compile", "--no-cache"]) + .arg(&entry) + .arg("--output") + .arg(&output) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + let run = Command::new(output) + .current_dir(dir.path()) + .output() + .expect("run compiled binary"); + assert!( + run.status.success(), + "run failed ({:?})\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + String::from_utf8(run.stdout).expect("UTF-8 stdout") +} + +const ISSUE_REPRO: &str = r#" +const Proto: any = { of(self: any) { return self } } +const Key = function () { function K() {}; Object.setPrototypeOf(K, Proto); return function (key: string) { (K as any).key = key; return K } } +const t = (name: string, f: () => any) => { try { console.log(name, JSON.stringify(f())) } catch (e: any) { console.log(name, "THROW", e.message) } } +class Base { static of(x: any) { return x } static b = 1 } +export namespace N { + export class Plain { static p = 7; static m() { return "m" } } + export class FromBase extends Base {} + export class FromCall extends (Key as any)()("@x/FromCall") {} + export const inside = () => [typeof Plain, Plain.p, Plain.m(), typeof FromBase, FromBase.b, FromBase.of({ a: 1 }), typeof FromCall] + export const ofInside = () => FromCall.of({ z: 1 }) + export const keyInside = () => (FromCall as any).key +} +t("C1 inside: Plain/FromBase/FromCall", () => N.inside()) +t("C2 outside N.Plain", () => [typeof N.Plain, N.Plain.p, N.Plain.m()]) +t("C3 outside N.FromBase", () => [typeof N.FromBase, N.FromBase.b, N.FromBase.of({ b: 2 })]) +t("C4 outside N.FromCall", () => [typeof N.FromCall, (N.FromCall as any).key]) +t("C5 outside N.FromCall.of", () => N.FromCall.of({ c: 3 })) +t("C6 keys of N", () => Object.keys(N)) +t("C7 inside FromCall.of", () => N.ofInside()) +t("C8 inside FromCall.key", () => N.keyInside()) +"#; + +#[test] +fn issue_repro_matches_bun() { + assert_eq!( + compile_and_run(ISSUE_REPRO), + concat!( + "C1 inside: Plain/FromBase/FromCall [\"function\",7,\"m\",\"function\",1,{\"a\":1},\"function\"]\n", + "C2 outside N.Plain [\"function\",7,\"m\"]\n", + "C3 outside N.FromBase [\"function\",1,{\"b\":2}]\n", + "C4 outside N.FromCall [\"function\",\"@x/FromCall\"]\n", + "C5 outside N.FromCall.of {\"c\":3}\n", + "C6 keys of N [\"Plain\",\"FromBase\",\"FromCall\",\"inside\",\"ofInside\",\"keyInside\"]\n", + "C7 inside FromCall.of {\"z\":1}\n", + "C8 inside FromCall.key \"@x/FromCall\"\n", + ) + ); +} + +const NESTED_AND_PRIVATE: &str = r#" +class C { static value = "top" } +export namespace Outer { + export const first = 1; + console.log("keys during", Object.keys(Outer).join(",")); + export class C { static value = "outer"; value = 9 } + export function readPrivate() { return new ReleaseError().message } + class ReleaseError { static prefix = "release"; message = ReleaseError.prefix + " failed" } + export namespace Inner { + export const before = 2; + export class C { static value = "inner"; value = 11 } + export const after = 3; + } + export const last = 4; +} +namespace Local { + export class C { static value = "local" } +} +console.log(C.value, Outer.C.value, Outer.Inner.C.value, Local.C.value); +console.log(Outer.readPrivate(), typeof (Outer as any).ReleaseError); +const read = Outer.readPrivate; +console.log(read()); +const outer = new Outer.C(); +const inner = new Outer.Inner.C(); +console.log(outer.value, inner.value, outer instanceof Outer.C, inner instanceof Outer.Inner.C, inner instanceof Outer.C); +console.log(Outer.C.name, Outer.Inner.C.name, Local.C.name); +console.log(JSON.stringify(Object.keys(Outer))); +console.log(JSON.stringify(Object.keys(Outer.Inner))); +"#; + +#[test] +fn nested_and_private_classes_keep_their_bindings() { + assert_eq!( + compile_and_run(NESTED_AND_PRIVATE), + concat!( + "keys during first\n", + "top outer inner local\n", + "release failed undefined\n", + "release failed\n", + "9 11 true true false\n", + "C C C\n", + "[\"first\",\"C\",\"readPrivate\",\"Inner\",\"last\"]\n", + "[\"before\",\"C\",\"after\"]\n", + ) + ); +} + +const CLASS_EVALUATION: &str = r#" +const events: string[] = []; +function key(name: string) { events.push("key:" + name); return name } +function init(name: string, value: number) { events.push("init:" + name); return value } +function decorate(target: any) { events.push("decorate:" + target.name); target.decorated = target.a + target.b } +function parent() { events.push("heritage"); return class { static inherited = 5 } } +export namespace Evaluation { + export class C extends parent() { + static [key("a")] = init("a", 7); + static { events.push("block:" + this.a); } + static [key("b")] = init("b", this.inherited + this.a); + [key("method")]() { return "method" } + } + @decorate + export class D { static a = C.a; static b = C.b } +} +console.log(JSON.stringify(events)); +console.log(Evaluation.C.a, Evaluation.C.b, Evaluation.D.decorated, new Evaluation.C().method()); +"#; + +#[test] +fn computed_names_static_blocks_and_decorators_run_in_order() { + assert_eq!( + compile_and_run(CLASS_EVALUATION), + concat!( + "[\"heritage\",\"key:a\",\"key:b\",\"key:method\",\"init:a\",\"block:7\",\"init:b\",\"decorate:D\"]\n", + "7 12 19 method\n", + ) + ); +} + +const TAGGED_ERROR: &str = r#" +const Schema = { + TaggedErrorClass() { + return (tag: string, fields: any) => class extends Error { + static tag = tag; + static of(value: any) { return value } + constructor(value: any) { super(value.message); this._tag = tag } + } + } +}; +export namespace Errors { + export class Failure extends Schema.TaggedErrorClass()("Failure", {}) {} + class ReleaseError extends Schema.TaggedErrorClass()("ReleaseError", {}) {} + export function release() { return new ReleaseError({ message: "release" })._tag } + export const inside = () => Failure.of({ value: 7 }); +} +const failure = new Errors.Failure({ message: "failed" }); +console.log(failure._tag, failure.message, failure instanceof Errors.Failure, failure instanceof Error); +console.log(Errors.Failure.tag, Errors.release()); +console.log(JSON.stringify(Errors.inside()), JSON.stringify(Errors.Failure.of({ value: 9 }))); +"#; + +#[test] +fn tagged_error_factory_heritage_matches_bun() { + assert_eq!( + compile_and_run(TAGGED_ERROR), + "Failure failed true true\nFailure ReleaseError\n{\"value\":7} {\"value\":9}\n" + ); +} diff --git a/crates/perry/tests/namespace_function_values.rs b/crates/perry/tests/namespace_function_values.rs new file mode 100644 index 0000000000..6dc7e7abc9 --- /dev/null +++ b/crates/perry/tests/namespace_function_values.rs @@ -0,0 +1,126 @@ +//! #10222 follow-up: an exported function of a TypeScript namespace referenced as a +//! VALUE from inside the namespace body (`const f = g`, `call(g)`, `Effect.gen(g)`) +//! must be the same callable that `NS.g` yields from outside. Direct calls were +//! already redirected to the namespace's static method; value references still +//! lowered to a module-function reference that had no body behind it. + +use std::path::PathBuf; +use std::process::Command; +use std::sync::Once; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("canonicalize workspace root") +} + +fn runtime_dir() -> PathBuf { + static BUILD_RUNTIME: Once = Once::new(); + BUILD_RUNTIME.call_once(|| { + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let build = Command::new(cargo) + .current_dir(workspace_root()) + .arg("build") + .arg("-p") + .arg("perry-runtime-static") + .arg("-p") + .arg("perry-stdlib-static") + .output() + .expect("build static runtime archives"); + assert!( + build.status.success(), + "static runtime build failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&build.stdout), + String::from_utf8_lossy(&build.stderr) + ); + }); + let target = std::env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| workspace_root().join("target")); + target.join("debug") +} + +const SOURCE: &str = r#" +const t = (name: string, f: () => any) => { try { console.log(name, JSON.stringify(f())) } catch (e: any) { console.log(name, "THROW", e.message) } } +const call = (f: any) => f() +const gen = (body: () => Generator) => { const it = body(); let r = it.next(); while (!r.done) r = it.next(r.value); return r.value } +export namespace NS { + export function g() { return 1 } + function h() { return 2 } + export function* gg() { const a: any = yield 1; return a + 1 } + export const v1 = () => { const f = g; return f() } + export const v2 = () => [g].map((f) => f()) + export const v3 = () => call(g) + export const v4 = () => call(h) + export const v5 = () => [typeof g, typeof h, g === NS.g] + export const v6 = () => call(gg).next().value + export const v7 = () => gen(gg) + export const v8 = () => g.name + export const v9 = () => call(v1) + export const v10 = () => g() +} +t("V1", () => NS.v1()) +t("V2", () => NS.v2()) +t("V3", () => NS.v3()) +t("V4", () => NS.v4()) +t("V5", () => NS.v5()) +t("V6", () => NS.v6()) +t("V7", () => NS.v7()) +t("V8", () => NS.v8()) +t("V9", () => NS.v9()) +t("V10", () => NS.v10()) +t("O1", () => { const f = NS.g; return f() }) +t("O2", () => call(NS.gg).next().value) +"#; + +const EXPECTED: &str = "V1 1 +V2 [1] +V3 1 +V4 2 +V5 [\"function\",\"function\",true] +V6 1 +V7 2 +V8 \"g\" +V9 1 +V10 1 +O1 1 +O2 1 +"; + +#[test] +fn exported_namespace_functions_are_first_class_inside_the_namespace() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, SOURCE).expect("write entry"); + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .env("PERRY_NO_AUTO_OPTIMIZE", "1") + .env("PERRY_RUNTIME_DIR", runtime_dir()) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + let run = Command::new(&output).current_dir(dir.path()).output().expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!(String::from_utf8_lossy(&run.stdout), EXPECTED); +}