From 459665de3cb135874892a1b75c2db8dde017fb39 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 20 Apr 2026 09:55:03 -0700 Subject: [PATCH 1/3] fix(codegen): arrow literal assigned to class field in ctor emits @ sigil on lambda ref (#587) --- src/codegen/infrastructure/assignment-generator.ts | 3 +++ tests/fixtures/classes/class-arrow-field-in-ctor.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 tests/fixtures/classes/class-arrow-field-in-ctor.ts diff --git a/src/codegen/infrastructure/assignment-generator.ts b/src/codegen/infrastructure/assignment-generator.ts index cbb494aa..c56a9c66 100644 --- a/src/codegen/infrastructure/assignment-generator.ts +++ b/src/codegen/infrastructure/assignment-generator.ts @@ -419,6 +419,9 @@ export class AssignmentGenerator { value: string, memberAccessValue: MemberAccessAssignmentNode, ): void { + if (value.startsWith("__lambda_")) { + value = `@${value}`; + } if (fiTsType) { const enumResult = this.isEnumType(fiTsType); if (enumResult) { diff --git a/tests/fixtures/classes/class-arrow-field-in-ctor.ts b/tests/fixtures/classes/class-arrow-field-in-ctor.ts new file mode 100644 index 00000000..60b5d299 --- /dev/null +++ b/tests/fixtures/classes/class-arrow-field-in-ctor.ts @@ -0,0 +1,12 @@ +// @test-description: issue #587 — arrow literal assigned to class field inside ctor body +class Foo { + cb: (x: number) => number; + constructor() { + this.cb = (x) => x * 2; + } +} + +const f = new Foo(); +if (f.cb(21) === 42) { + console.log("TEST_PASSED"); +} From 887c76fc04c09207cef85ec76c94d2f6ec410608 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 20 Apr 2026 09:55:09 -0700 Subject: [PATCH 2/3] fix(parser): accept ElementAccessExpression as call callee (#589) --- src/parser-ts/handlers/expressions.ts | 3 ++- .../edge-cases/call-element-access-callee.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/edge-cases/call-element-access-callee.ts diff --git a/src/parser-ts/handlers/expressions.ts b/src/parser-ts/handlers/expressions.ts index c3e75f93..e3879f27 100644 --- a/src/parser-ts/handlers/expressions.ts +++ b/src/parser-ts/handlers/expressions.ts @@ -435,7 +435,8 @@ function transformCallExpression( }; } else if ( ts.isCallExpression(node.expression) || - ts.isParenthesizedExpression(node.expression) + ts.isParenthesizedExpression(node.expression) || + ts.isElementAccessExpression(node.expression) ) { const callee = transformExpression(node.expression, checker); return { diff --git a/tests/fixtures/edge-cases/call-element-access-callee.ts b/tests/fixtures/edge-cases/call-element-access-callee.ts new file mode 100644 index 00000000..947f6762 --- /dev/null +++ b/tests/fixtures/edge-cases/call-element-access-callee.ts @@ -0,0 +1,12 @@ +// @test-description: issue #589 — parser accepts arr[i](args) as a call expression (ElementAccessExpression callee) +// @test-compile-error: Immediately invoked function expressions +function doubler(x: number): number { + return x * 2; +} +function plusOne(x: number): number { + return x + 1; +} + +const fns = [doubler, plusOne]; +const a = fns[0](10); +console.log(a); From d50c519990254e2c1bd950a47de392a6fb28ae7c Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 20 Apr 2026 09:55:13 -0700 Subject: [PATCH 3/3] fix(ffi): null char* from declare-function round-trips to empty string (#591) --- src/codegen/expressions/calls.ts | 14 ++++++++++++++ .../edge-cases/ffi-null-string-to-empty.ts | 9 +++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/fixtures/edge-cases/ffi-null-string-to-empty.ts diff --git a/src/codegen/expressions/calls.ts b/src/codegen/expressions/calls.ts index 8b9ecb56..36dd6ff8 100644 --- a/src/codegen/expressions/calls.ts +++ b/src/codegen/expressions/calls.ts @@ -1170,6 +1170,20 @@ export class CallExpressionGenerator { return coerced; } + // FFI null-string coercion: `declare function f(): string` returning NULL + // from C should round-trip to TS as the empty string so `result === ""` + // works reliably. Only applied to user-declared extern functions (not + // internal runtime calls, which rely on NULL sentinels internally). + if (returnType === "i8*" && func && func.declare) { + const emptyStr = this.ctx.stringGen.doCreateStringConstant(""); + const isNull = this.ctx.nextTemp(); + this.ctx.emit(`${isNull} = icmp eq i8* ${temp}, null`); + const coerced = this.ctx.nextTemp(); + this.ctx.emit(`${coerced} = select i1 ${isNull}, i8* ${emptyStr}, i8* ${temp}`); + this.ctx.setVariableType(coerced, "i8*"); + return coerced; + } + return temp; } diff --git a/tests/fixtures/edge-cases/ffi-null-string-to-empty.ts b/tests/fixtures/edge-cases/ffi-null-string-to-empty.ts new file mode 100644 index 00000000..56d81ff3 --- /dev/null +++ b/tests/fixtures/edge-cases/ffi-null-string-to-empty.ts @@ -0,0 +1,9 @@ +// @test-description: issue #591 — NULL char* from FFI round-trips to TS as empty string +declare function getenv(name: string): string; + +const v = getenv("__CHADSCRIPT_TEST_VAR_THAT_DOES_NOT_EXIST_91237__"); +if (v === "") { + console.log("TEST_PASSED"); +} else { + console.log("FAIL: got " + v); +}