@jalvin/compiler 2.0.12 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,12 +73,14 @@ const JS_GLOBALS = new Set([
73
73
  "fetch", "URL", "URLSearchParams", "FormData", "Blob", "File",
74
74
  "localStorage", "sessionStorage", "performance", "navigator", "location",
75
75
  "HTMLElement", "Element", "Event", "CustomEvent",
76
+ "confirm", "alert", "prompt",
76
77
  ]);
77
78
  function nullable(t) {
78
79
  if (t.tag === "nullable")
79
80
  return t;
80
- if (t.tag === "nothing")
81
- return exports.T_UNIT; // Nothing? Unit?
81
+ // Note: Nothing? is kept as nullable(nothing), not collapsed to Unit.
82
+ // In Kotlin semantics Nothing? is the type of `null` and is assignable
83
+ // to every nullable type, which isAssignable() handles correctly.
82
84
  return { tag: "nullable", inner: t };
83
85
  }
84
86
  function unwrapNullable(t) {
@@ -548,6 +550,7 @@ class TypeChecker {
548
550
  }
549
551
  if (!decl.body)
550
552
  return;
553
+ const body = decl.body;
551
554
  const seen = new Set();
552
555
  // Collect all member names from parent class (for override/abstract checks)
553
556
  const parentMemberNames = new Set();
@@ -568,26 +571,75 @@ class TypeChecker {
568
571
  }
569
572
  }
570
573
  const concreteMembers = new Set();
571
- for (const member of decl.body.members) {
572
- const mName = this.memberName(member);
573
- if (mName) {
574
- if (seen.has(mName)) {
575
- this.diag.error(member.span, diagnostics_js_1.E_DUPLICATE_CLASS_MEMBER, `Duplicate member '${mName}'`);
574
+ // Build a class-level scope so that methods can call sibling members and
575
+ // reference `this` by bare name — mirroring Kotlin's behaviour.
576
+ const classScope = new Scope(this.scope);
577
+ classScope.define({
578
+ name: "this",
579
+ type: classType(decl.name, [], decl),
580
+ mutable: false,
581
+ span: decl.span,
582
+ });
583
+ // Hoist primary-constructor val/var params as class properties
584
+ const ctor = decl.primaryConstructor
585
+ ?? decl.primaryConstructor
586
+ ?? null;
587
+ if (ctor) {
588
+ for (const p of ctor.params) {
589
+ if (p.propertyKind) {
590
+ classScope.define({
591
+ name: p.name,
592
+ type: this.resolveTypeRef(p.type),
593
+ mutable: p.propertyKind === "var",
594
+ span: p.span,
595
+ });
576
596
  }
577
- seen.add(mName);
578
- // Check `override` is actually overriding something
579
- if ((member.kind === "FunDecl" || member.kind === "PropertyDecl") &&
580
- member.modifiers.modifiers.includes("override")) {
581
- if (!parentMemberNames.has(mName)) {
582
- this.diag.error(member.span, diagnostics_js_1.E_OVERRIDE_NOTHING, `'${mName}' overrides nothing`);
597
+ }
598
+ }
599
+ // Hoist all class body members so they're visible to each other
600
+ for (const member of body.members) {
601
+ if (member.kind === "FunDecl") {
602
+ classScope.define({ name: member.name, type: this.funDeclType(member), mutable: false, span: member.span });
603
+ }
604
+ else if (member.kind === "PropertyDecl") {
605
+ classScope.define({
606
+ name: member.name,
607
+ type: member.type ? this.resolveTypeRef(member.type) : exports.T_UNKNOWN,
608
+ mutable: member.mutable,
609
+ span: member.span,
610
+ });
611
+ }
612
+ else if (member.kind === "ComponentDecl") {
613
+ classScope.define({
614
+ name: member.name,
615
+ type: { tag: "func", params: this.componentParamTypes(member), ret: exports.T_UNIT, suspend: false },
616
+ mutable: false,
617
+ span: member.span,
618
+ });
619
+ }
620
+ }
621
+ this.withScope(classScope, () => {
622
+ for (const member of body.members) {
623
+ const mName = this.memberName(member);
624
+ if (mName) {
625
+ if (seen.has(mName)) {
626
+ this.diag.error(member.span, diagnostics_js_1.E_DUPLICATE_CLASS_MEMBER, `Duplicate member '${mName}'`);
627
+ }
628
+ seen.add(mName);
629
+ // Check `override` is actually overriding something
630
+ if ((member.kind === "FunDecl" || member.kind === "PropertyDecl") &&
631
+ member.modifiers.modifiers.includes("override")) {
632
+ if (!parentMemberNames.has(mName)) {
633
+ this.diag.error(member.span, diagnostics_js_1.E_OVERRIDE_NOTHING, `'${mName}' overrides nothing`);
634
+ }
635
+ }
636
+ if (!(member.kind === "FunDecl" && member.modifiers.modifiers.includes("abstract"))) {
637
+ concreteMembers.add(mName);
583
638
  }
584
639
  }
585
- if (!(member.kind === "FunDecl" && member.modifiers.modifiers.includes("abstract"))) {
586
- concreteMembers.add(mName);
587
- }
640
+ this.checkClassMember(member);
588
641
  }
589
- this.checkClassMember(member);
590
- }
642
+ });
591
643
  // Check all abstract parent members are implemented (only for non-abstract, non-sealed subclasses)
592
644
  const isAbstract = decl.modifiers.modifiers.includes("abstract");
593
645
  const isSealed = decl.kind === "SealedClassDecl";
@@ -791,6 +843,7 @@ class TypeChecker {
791
843
  * Extract smart-cast bindings from an `is`-check condition.
792
844
  * `x is T` → { x: T }
793
845
  * `x is T && y is U` → { x: T, y: U }
846
+ * `x != null` → { x: unwrapped(x) }
794
847
  */
795
848
  extractSmartCasts(condition) {
796
849
  const result = new Map();
@@ -799,6 +852,15 @@ class TypeChecker {
799
852
  result.set(condition.expr.name, this.resolveTypeRef(condition.type));
800
853
  }
801
854
  }
855
+ else if (condition.kind === "BinaryExpr" &&
856
+ (condition.op === "!=" || condition.op === "!==") &&
857
+ condition.right.kind === "NullLiteralExpr" &&
858
+ condition.left.kind === "NameExpr") {
859
+ // x != null → narrow x to its non-nullable type inside then-branch
860
+ const varType = this.scope.lookup(condition.left.name)?.type;
861
+ if (varType)
862
+ result.set(condition.left.name, unwrapNullable(varType));
863
+ }
802
864
  else if (condition.kind === "BinaryExpr" && condition.op === "&&") {
803
865
  for (const [k, v] of this.extractSmartCasts(condition.left))
804
866
  result.set(k, v);
@@ -811,6 +873,7 @@ class TypeChecker {
811
873
  * Extract smart-cast bindings for the ELSE branch.
812
874
  * `x !is T` → in else, `x` IS `T` → { x: T }
813
875
  * `x !is T || y !is U` → in else (both negations must fail), { x: T, y: U }
876
+ * `x == null` → in else, x is non-null → { x: unwrapped(x) }
814
877
  */
815
878
  extractElseSmartCasts(condition) {
816
879
  const result = new Map();
@@ -820,6 +883,15 @@ class TypeChecker {
820
883
  result.set(condition.expr.name, this.resolveTypeRef(condition.type));
821
884
  }
822
885
  }
886
+ else if (condition.kind === "BinaryExpr" &&
887
+ (condition.op === "==" || condition.op === "===") &&
888
+ condition.right.kind === "NullLiteralExpr" &&
889
+ condition.left.kind === "NameExpr") {
890
+ // x == null → in else branch, x is non-null
891
+ const varType = this.scope.lookup(condition.left.name)?.type;
892
+ if (varType)
893
+ result.set(condition.left.name, unwrapNullable(varType));
894
+ }
823
895
  else if (condition.kind === "BinaryExpr" && condition.op === "||") {
824
896
  // x !is T || y !is U → both must be false to reach else, so both narrow
825
897
  for (const [k, v] of this.extractElseSmartCasts(condition.left))
@@ -1003,6 +1075,9 @@ class TypeChecker {
1003
1075
  // Don't error on Bibi — it's a special runtime symbol
1004
1076
  if (expr.name === "Bibi")
1005
1077
  return { tag: "func", params: [exports.T_STRING], ret: classType("BibiClient"), suspend: false };
1078
+ // Unit singleton — usable as a value (e.g. LaunchedEffect(Unit))
1079
+ if (expr.name === "Unit")
1080
+ return classType("Unit");
1006
1081
  // Companion-like type objects (for Int.MAX_VALUE, Long.MIN_VALUE)
1007
1082
  if (expr.name === "Int")
1008
1083
  return classType("IntCompanion");
@@ -1079,6 +1154,20 @@ class TypeChecker {
1079
1154
  return exports.T_UNKNOWN;
1080
1155
  }
1081
1156
  case "CallExpr": return this.checkCallExpr(expr);
1157
+ case "SafeCallExpr": {
1158
+ const calleeType = this.checkExpr(expr.callee);
1159
+ for (const arg of expr.args)
1160
+ this.checkExpr(arg.value);
1161
+ if (expr.trailingLambda)
1162
+ this.checkLambda(expr.trailingLambda);
1163
+ const inner = unwrapNullable(calleeType);
1164
+ if (inner.tag === "func")
1165
+ return nullable(inner.ret);
1166
+ if (inner.tag === "any" || inner.tag === "unknown" || inner.tag === "error")
1167
+ return exports.T_UNKNOWN;
1168
+ this.diag.error(expr.span, diagnostics_js_1.E_NOT_A_FUNCTION, `Type '${this.typeToString(calleeType)}' is not callable`);
1169
+ return exports.T_ERROR;
1170
+ }
1082
1171
  case "LambdaExpr": return this.checkLambda(expr);
1083
1172
  case "IfExpr": return this.checkIfExpr(expr);
1084
1173
  case "WhenExpr": {
@@ -1345,12 +1434,16 @@ class TypeChecker {
1345
1434
  }
1346
1435
  checkLambda(expr, expectedParamTypes) {
1347
1436
  const childScope = new Scope(this.scope);
1348
- // If the lambda has no explicit params, synthesise an `it` binding using
1349
- // the first expected parameter type.
1350
- if (expr.params.length === 0 && expectedParamTypes && expectedParamTypes.length === 1) {
1437
+ // If the lambda has no explicit params, synthesise an `it` binding.
1438
+ // Use the expected parameter type when available, otherwise T_UNKNOWN.
1439
+ // This ensures `it` is always resolvable in single-param lambdas.
1440
+ if (expr.params.length === 0) {
1441
+ const itType = (expectedParamTypes && expectedParamTypes.length === 1)
1442
+ ? expectedParamTypes[0]
1443
+ : exports.T_UNKNOWN;
1351
1444
  childScope.define({
1352
1445
  name: "it",
1353
- type: expectedParamTypes[0],
1446
+ type: itType,
1354
1447
  mutable: false,
1355
1448
  span: expr.span,
1356
1449
  });
@@ -2165,6 +2258,9 @@ class TypeChecker {
2165
2258
  return true;
2166
2259
  if (from.tag === "unknown" || to.tag === "unknown")
2167
2260
  return true;
2261
+ // nullable(Nothing) is the type of the `null` literal — assignable to every nullable type
2262
+ if (from.tag === "nullable" && from.inner.tag === "nothing")
2263
+ return to.tag === "nullable";
2168
2264
  if (to.tag === "nullable")
2169
2265
  return this.isAssignable(from, to.inner) || from.tag === "nullable";
2170
2266
  if (from.tag === "nullable")