@jalvin/compiler 2.0.32 → 2.0.34

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.
Files changed (57) hide show
  1. package/dist/codegen/index.d.ts +179 -0
  2. package/dist/codegen/index.d.ts.map +1 -0
  3. package/dist/codegen/index.js +2207 -0
  4. package/dist/codegen/index.js.map +1 -0
  5. package/dist/codegen.d.ts +1 -1
  6. package/dist/codegen.d.ts.map +1 -1
  7. package/dist/codegen.js +64 -23
  8. package/dist/codegen.js.map +1 -1
  9. package/dist/codegen_freestanding_c.d.ts +9 -0
  10. package/dist/codegen_freestanding_c.d.ts.map +1 -0
  11. package/dist/codegen_freestanding_c.js +321 -0
  12. package/dist/codegen_freestanding_c.js.map +1 -0
  13. package/dist/diagnostics.d.ts +2 -0
  14. package/dist/diagnostics.d.ts.map +1 -1
  15. package/dist/diagnostics.js +3 -1
  16. package/dist/diagnostics.js.map +1 -1
  17. package/dist/lexer/chars.d.ts +5 -0
  18. package/dist/lexer/chars.d.ts.map +1 -0
  19. package/dist/lexer/chars.js +20 -0
  20. package/dist/lexer/chars.js.map +1 -0
  21. package/dist/lexer/index.d.ts +37 -0
  22. package/dist/lexer/index.d.ts.map +1 -0
  23. package/dist/lexer/index.js +630 -0
  24. package/dist/lexer/index.js.map +1 -0
  25. package/dist/lexer/tokens.d.ts +135 -0
  26. package/dist/lexer/tokens.d.ts.map +1 -0
  27. package/dist/lexer/tokens.js +89 -0
  28. package/dist/lexer/tokens.js.map +1 -0
  29. package/dist/parser/constants.d.ts +3 -0
  30. package/dist/parser/constants.d.ts.map +1 -0
  31. package/dist/parser/constants.js +7 -0
  32. package/dist/parser/constants.js.map +1 -0
  33. package/dist/parser/helpers.d.ts +2 -0
  34. package/dist/parser/helpers.d.ts.map +1 -0
  35. package/dist/parser/helpers.js +9 -0
  36. package/dist/parser/helpers.js.map +1 -0
  37. package/dist/parser/index.d.ts +118 -0
  38. package/dist/parser/index.d.ts.map +1 -0
  39. package/dist/parser/index.js +2387 -0
  40. package/dist/parser/index.js.map +1 -0
  41. package/dist/typechecker/globals.d.ts +2 -0
  42. package/dist/typechecker/globals.d.ts.map +1 -0
  43. package/dist/typechecker/globals.js +15 -0
  44. package/dist/typechecker/globals.js.map +1 -0
  45. package/dist/typechecker/index.d.ts +146 -0
  46. package/dist/typechecker/index.d.ts.map +1 -0
  47. package/dist/typechecker/index.js +2288 -0
  48. package/dist/typechecker/index.js.map +1 -0
  49. package/dist/typechecker/types.d.ts +66 -0
  50. package/dist/typechecker/types.d.ts.map +1 -0
  51. package/dist/typechecker/types.js +39 -0
  52. package/dist/typechecker/types.js.map +1 -0
  53. package/dist/typechecker.d.ts +12 -0
  54. package/dist/typechecker.d.ts.map +1 -1
  55. package/dist/typechecker.js +243 -149
  56. package/dist/typechecker.js.map +1 -1
  57. package/package.json +10 -15
@@ -243,6 +243,10 @@ class TypeChecker {
243
243
  inSuspend = false;
244
244
  /** Whether we're inside a component */
245
245
  inComponent = false;
246
+ /** Nesting depth where component-only APIs would violate stable call order. */
247
+ componentCallOrderDepth = 0;
248
+ /** Context stack describing where call-order nesting was introduced. */
249
+ componentCallOrderContextStack = [];
246
250
  /** Whether the program has a wildcard import from a @jalvin/* package */
247
251
  hasWildcardJalvinImport = false;
248
252
  /** Map from node to resolved type (for IDE use) */
@@ -797,12 +801,14 @@ class TypeChecker {
797
801
  this.checkWhileStmt(stmt);
798
802
  break;
799
803
  case "DoWhileStmt": {
800
- this.checkBlock(stmt.body);
801
- this.checkExpr(stmt.condition);
804
+ this.withComponentOrderNesting("do-while loop", () => {
805
+ this.checkBlock(stmt.body);
806
+ this.checkExpr(stmt.condition);
807
+ });
802
808
  break;
803
809
  }
804
810
  case "TryCatchStmt":
805
- this.checkTryCatch(stmt);
811
+ this.withComponentOrderNesting("try/catch statement", () => this.checkTryCatch(stmt));
806
812
  break;
807
813
  case "LabeledStmt":
808
814
  this.checkStmt(stmt.body);
@@ -810,41 +816,43 @@ class TypeChecker {
810
816
  }
811
817
  }
812
818
  checkIfStmt(stmt) {
813
- const condType = this.checkExpr(stmt.condition);
814
- this.assertAssignable(stmt.condition.span, condType, exports.T_BOOL);
815
- // Smart cast: if (x is T) { ... } narrows x to T inside the then branch
816
- const narrowings = this.extractSmartCasts(stmt.condition);
817
- const thenScope = new Scope(this.scope);
818
- for (const [name, type] of narrowings) {
819
- const existing = this.scope.lookup(name);
820
- if (existing) {
821
- thenScope.define({ ...existing, type });
822
- }
823
- }
824
- this.withScope(thenScope, () => this.checkBlock(stmt.then));
825
- if (stmt.else) {
826
- const elseNarrowings = this.extractElseSmartCasts(stmt.condition);
827
- if (elseNarrowings.size > 0) {
828
- const elseScope = new Scope(this.scope);
829
- for (const [name, type] of elseNarrowings) {
830
- const existing = this.scope.lookup(name);
831
- if (existing)
832
- elseScope.define({ ...existing, type });
819
+ this.withComponentOrderNesting("if statement", () => {
820
+ const condType = this.checkExpr(stmt.condition);
821
+ this.assertAssignable(stmt.condition.span, condType, exports.T_BOOL);
822
+ // Smart cast: if (x is T) { ... } narrows x to T inside the then branch
823
+ const narrowings = this.extractSmartCasts(stmt.condition);
824
+ const thenScope = new Scope(this.scope);
825
+ for (const [name, type] of narrowings) {
826
+ const existing = this.scope.lookup(name);
827
+ if (existing) {
828
+ thenScope.define({ ...existing, type });
833
829
  }
834
- this.withScope(elseScope, () => {
830
+ }
831
+ this.withScope(thenScope, () => this.checkBlock(stmt.then));
832
+ if (stmt.else) {
833
+ const elseNarrowings = this.extractElseSmartCasts(stmt.condition);
834
+ if (elseNarrowings.size > 0) {
835
+ const elseScope = new Scope(this.scope);
836
+ for (const [name, type] of elseNarrowings) {
837
+ const existing = this.scope.lookup(name);
838
+ if (existing)
839
+ elseScope.define({ ...existing, type });
840
+ }
841
+ this.withScope(elseScope, () => {
842
+ if (stmt.else.kind === "Block")
843
+ this.checkBlock(stmt.else);
844
+ else if (stmt.else.kind === "IfStmt")
845
+ this.checkIfStmt(stmt.else);
846
+ });
847
+ }
848
+ else {
835
849
  if (stmt.else.kind === "Block")
836
850
  this.checkBlock(stmt.else);
837
851
  else if (stmt.else.kind === "IfStmt")
838
852
  this.checkIfStmt(stmt.else);
839
- });
840
- }
841
- else {
842
- if (stmt.else.kind === "Block")
843
- this.checkBlock(stmt.else);
844
- else if (stmt.else.kind === "IfStmt")
845
- this.checkIfStmt(stmt.else);
853
+ }
846
854
  }
847
- }
855
+ });
848
856
  }
849
857
  /**
850
858
  * Extract smart-cast bindings from an `is`-check condition.
@@ -909,61 +917,63 @@ class TypeChecker {
909
917
  return result;
910
918
  }
911
919
  checkWhenStmt(stmt) {
912
- const subjectType = stmt.subject ? this.checkExpr(stmt.subject.expr) : null;
913
- // Bind `when (val x = expr)` into each branch scope
914
- for (const branch of stmt.branches) {
915
- const branchScope = stmt.subject?.binding
916
- ? new Scope(this.scope)
917
- : this.scope;
918
- if (stmt.subject?.binding && subjectType) {
919
- branchScope.define({
920
- name: stmt.subject.binding,
921
- type: subjectType,
922
- mutable: false,
923
- span: stmt.subject.span,
924
- });
925
- }
926
- this.withScope(branchScope, () => {
927
- // Smart cast narrowing: for single `is Type` condition, narrow subject type
928
- if (stmt.subject?.binding &&
929
- subjectType &&
930
- branch.conditions.length === 1 &&
931
- branch.conditions[0].kind === "WhenIsCondition" &&
932
- !branch.conditions[0].negated) {
933
- const isC = branch.conditions[0];
934
- const narrowedType = this.resolveTypeRef(isC.type);
935
- const narrowScope = new Scope(branchScope);
936
- narrowScope.define({
920
+ this.withComponentOrderNesting("when statement", () => {
921
+ const subjectType = stmt.subject ? this.checkExpr(stmt.subject.expr) : null;
922
+ // Bind `when (val x = expr)` into each branch scope
923
+ for (const branch of stmt.branches) {
924
+ const branchScope = stmt.subject?.binding
925
+ ? new Scope(this.scope)
926
+ : this.scope;
927
+ if (stmt.subject?.binding && subjectType) {
928
+ branchScope.define({
937
929
  name: stmt.subject.binding,
938
- type: narrowedType,
930
+ type: subjectType,
939
931
  mutable: false,
940
- span: isC.span,
932
+ span: stmt.subject.span,
941
933
  });
942
- this.withScope(narrowScope, () => {
934
+ }
935
+ this.withScope(branchScope, () => {
936
+ // Smart cast narrowing: for single `is Type` condition, narrow subject type
937
+ if (stmt.subject?.binding &&
938
+ subjectType &&
939
+ branch.conditions.length === 1 &&
940
+ branch.conditions[0].kind === "WhenIsCondition" &&
941
+ !branch.conditions[0].negated) {
942
+ const isC = branch.conditions[0];
943
+ const narrowedType = this.resolveTypeRef(isC.type);
944
+ const narrowScope = new Scope(branchScope);
945
+ narrowScope.define({
946
+ name: stmt.subject.binding,
947
+ type: narrowedType,
948
+ mutable: false,
949
+ span: isC.span,
950
+ });
951
+ this.withScope(narrowScope, () => {
952
+ if (branch.body.kind === "Block")
953
+ this.checkBlock(branch.body);
954
+ else
955
+ this.checkExpr(branch.body);
956
+ });
957
+ }
958
+ else {
959
+ for (const cond of branch.conditions) {
960
+ if (cond.kind === "WhenExprCondition")
961
+ this.checkExpr(cond.expr);
962
+ else if (cond.kind === "WhenInCondition")
963
+ this.checkExpr(cond.expr);
964
+ }
943
965
  if (branch.body.kind === "Block")
944
966
  this.checkBlock(branch.body);
945
967
  else
946
968
  this.checkExpr(branch.body);
947
- });
948
- }
949
- else {
950
- for (const cond of branch.conditions) {
951
- if (cond.kind === "WhenExprCondition")
952
- this.checkExpr(cond.expr);
953
- else if (cond.kind === "WhenInCondition")
954
- this.checkExpr(cond.expr);
955
969
  }
956
- if (branch.body.kind === "Block")
957
- this.checkBlock(branch.body);
958
- else
959
- this.checkExpr(branch.body);
960
- }
961
- });
962
- }
963
- // Exhaustiveness check for sealed classes
964
- if (subjectType) {
965
- this.checkWhenExhaustiveness(stmt.span, stmt.branches, subjectType);
966
- }
970
+ });
971
+ }
972
+ // Exhaustiveness check for sealed classes
973
+ if (subjectType) {
974
+ this.checkWhenExhaustiveness(stmt.span, stmt.branches, subjectType);
975
+ }
976
+ });
967
977
  }
968
978
  /**
969
979
  * If `subjectType` is a sealed class or enum, verify all variants are
@@ -1030,17 +1040,21 @@ class TypeChecker {
1030
1040
  }
1031
1041
  }
1032
1042
  checkForStmt(stmt) {
1033
- const iterType = this.checkExpr(stmt.iterable);
1034
- const elemType = this.elementTypeOf(iterType);
1035
- const childScope = new Scope(this.scope);
1036
- if (typeof stmt.binding === "string") {
1037
- childScope.define({ name: stmt.binding, type: elemType, mutable: false, span: stmt.span });
1038
- }
1039
- this.withScope(childScope, () => this.checkBlock(stmt.body));
1043
+ this.withComponentOrderNesting("for loop", () => {
1044
+ const iterType = this.checkExpr(stmt.iterable);
1045
+ const elemType = this.elementTypeOf(iterType);
1046
+ const childScope = new Scope(this.scope);
1047
+ if (typeof stmt.binding === "string") {
1048
+ childScope.define({ name: stmt.binding, type: elemType, mutable: false, span: stmt.span });
1049
+ }
1050
+ this.withScope(childScope, () => this.checkBlock(stmt.body));
1051
+ });
1040
1052
  }
1041
1053
  checkWhileStmt(stmt) {
1042
- this.checkExpr(stmt.condition);
1043
- this.checkBlock(stmt.body);
1054
+ this.withComponentOrderNesting("while loop", () => {
1055
+ this.checkExpr(stmt.condition);
1056
+ this.checkBlock(stmt.body);
1057
+ });
1044
1058
  }
1045
1059
  checkTryCatch(stmt) {
1046
1060
  this.checkBlock(stmt.body);
@@ -1178,29 +1192,35 @@ class TypeChecker {
1178
1192
  case "LambdaExpr": return this.checkLambda(expr);
1179
1193
  case "IfExpr": return this.checkIfExpr(expr);
1180
1194
  case "WhenExpr": {
1181
- const subjectType = expr.subject ? this.checkExpr(expr.subject.expr) : null;
1182
- const branchTypes = [];
1183
- for (const b of expr.branches) {
1184
- for (const c of b.conditions) {
1185
- if (c.kind === "WhenExprCondition")
1186
- this.checkExpr(c.expr);
1187
- else if (c.kind === "WhenInCondition")
1188
- this.checkExpr(c.expr);
1195
+ let resultType = exports.T_UNKNOWN;
1196
+ this.withComponentOrderNesting("when expression", () => {
1197
+ const subjectType = expr.subject ? this.checkExpr(expr.subject.expr) : null;
1198
+ const branchTypes = [];
1199
+ for (const b of expr.branches) {
1200
+ for (const c of b.conditions) {
1201
+ if (c.kind === "WhenExprCondition")
1202
+ this.checkExpr(c.expr);
1203
+ else if (c.kind === "WhenInCondition")
1204
+ this.checkExpr(c.expr);
1205
+ }
1206
+ branchTypes.push(b.body.kind === "Block" ? (this.checkBlock(b.body), exports.T_UNIT) : this.checkExpr(b.body));
1189
1207
  }
1190
- branchTypes.push(b.body.kind === "Block" ? (this.checkBlock(b.body), exports.T_UNIT) : this.checkExpr(b.body));
1191
- }
1192
- // Exhaustiveness: when-expr on a sealed class MUST be exhaustive
1193
- if (subjectType) {
1194
- this.checkWhenExhaustiveness(expr.span, expr.branches, subjectType);
1195
- }
1196
- return this.unify(branchTypes);
1208
+ // Exhaustiveness: when-expr on a sealed class MUST be exhaustive
1209
+ if (subjectType) {
1210
+ this.checkWhenExhaustiveness(expr.span, expr.branches, subjectType);
1211
+ }
1212
+ resultType = this.unify(branchTypes);
1213
+ });
1214
+ return resultType;
1197
1215
  }
1198
1216
  case "TryCatchExpr": {
1199
- this.checkBlock(expr.body);
1200
- for (const c of expr.catches)
1201
- this.checkBlock(c.body);
1202
- if (expr.finally)
1203
- this.checkBlock(expr.finally);
1217
+ this.withComponentOrderNesting("try/catch expression", () => {
1218
+ this.checkBlock(expr.body);
1219
+ for (const c of expr.catches)
1220
+ this.checkBlock(c.body);
1221
+ if (expr.finally)
1222
+ this.checkBlock(expr.finally);
1223
+ });
1204
1224
  return exports.T_UNKNOWN;
1205
1225
  }
1206
1226
  case "TypeCheckExpr": {
@@ -1239,14 +1259,14 @@ class TypeChecker {
1239
1259
  }
1240
1260
  const prevSuspend = this.inSuspend;
1241
1261
  this.inSuspend = true;
1242
- this.checkBlock(expr.body);
1262
+ this.withComponentOrderNesting("launch block", () => this.checkBlock(expr.body));
1243
1263
  this.inSuspend = prevSuspend;
1244
1264
  return classType("Job");
1245
1265
  }
1246
1266
  case "AsyncExpr": {
1247
1267
  const prevSuspend = this.inSuspend;
1248
1268
  this.inSuspend = true;
1249
- this.checkBlock(expr.body);
1269
+ this.withComponentOrderNesting("async block", () => this.checkBlock(expr.body));
1250
1270
  this.inSuspend = prevSuspend;
1251
1271
  return classType("Deferred", [exports.T_UNKNOWN]);
1252
1272
  }
@@ -1395,6 +1415,27 @@ class TypeChecker {
1395
1415
  return null;
1396
1416
  }
1397
1417
  checkCallExpr(expr) {
1418
+ const componentOnlyName = this.getComponentOnlyCalleeName(expr.callee);
1419
+ if (componentOnlyName) {
1420
+ if (!this.inComponent) {
1421
+ this.diag.error(expr.span, diagnostics_js_1.E_COMPONENT_CONTEXT_REQUIRED, `'${componentOnlyName}' can only be called inside a component function`);
1422
+ }
1423
+ else if (this.componentCallOrderDepth > 0) {
1424
+ const context = this.currentComponentOrderContext();
1425
+ this.diag.error(expr.span, diagnostics_js_1.E_COMPONENT_CALL_ORDER, `'${componentOnlyName}' must be called unconditionally at the top level of a component function`, [
1426
+ {
1427
+ span: null,
1428
+ message: context
1429
+ ? `Invalid call occurs inside ${context}`
1430
+ : "Invalid call occurs inside nested control flow",
1431
+ },
1432
+ {
1433
+ span: null,
1434
+ message: "Move this call to the top-level component body",
1435
+ },
1436
+ ]);
1437
+ }
1438
+ }
1398
1439
  const calleeType = this.checkExpr(expr.callee);
1399
1440
  for (const arg of expr.args)
1400
1441
  this.checkExpr(arg.value);
@@ -1439,6 +1480,53 @@ class TypeChecker {
1439
1480
  this.diag.error(expr.span, diagnostics_js_1.E_NOT_A_FUNCTION, `Type '${this.typeToString(calleeType)}' is not callable`);
1440
1481
  return exports.T_ERROR;
1441
1482
  }
1483
+ /**
1484
+ * Runtime APIs that are only valid in component functions.
1485
+ * We key by callee name to keep checks fast and diagnostics clear.
1486
+ */
1487
+ isComponentOnlyApi(name) {
1488
+ return name === "mutableStateOf" ||
1489
+ name === "remember" ||
1490
+ name === "rememberMutableStateOf" ||
1491
+ name === "collectAsState" ||
1492
+ name === "useViewModel" ||
1493
+ name === "LaunchedEffect" ||
1494
+ name === "DisposableEffect" ||
1495
+ name === "SideEffect" ||
1496
+ name === "useMutableInteractionSource" ||
1497
+ name === "useIsHovered" ||
1498
+ name === "useIsFocused" ||
1499
+ name === "useIsPressed";
1500
+ }
1501
+ getComponentOnlyCalleeName(callee) {
1502
+ if (callee.kind === "NameExpr" && this.isComponentOnlyApi(callee.name)) {
1503
+ return callee.name;
1504
+ }
1505
+ if ((callee.kind === "MemberExpr" || callee.kind === "SafeMemberExpr") && this.isComponentOnlyApi(callee.member)) {
1506
+ return callee.member;
1507
+ }
1508
+ return null;
1509
+ }
1510
+ currentComponentOrderContext() {
1511
+ if (this.componentCallOrderContextStack.length === 0)
1512
+ return null;
1513
+ return this.componentCallOrderContextStack[this.componentCallOrderContextStack.length - 1];
1514
+ }
1515
+ withComponentOrderNesting(context, fn) {
1516
+ if (!this.inComponent) {
1517
+ fn();
1518
+ return;
1519
+ }
1520
+ this.componentCallOrderDepth++;
1521
+ this.componentCallOrderContextStack.push(context);
1522
+ try {
1523
+ fn();
1524
+ }
1525
+ finally {
1526
+ this.componentCallOrderContextStack.pop();
1527
+ this.componentCallOrderDepth--;
1528
+ }
1529
+ }
1442
1530
  checkLambda(expr, expectedParamTypes) {
1443
1531
  const childScope = new Scope(this.scope);
1444
1532
  // If the lambda has no explicit params, synthesise an `it` binding.
@@ -1467,12 +1555,14 @@ class TypeChecker {
1467
1555
  }
1468
1556
  let retType = exports.T_UNIT;
1469
1557
  this.withScope(childScope, () => {
1470
- for (const stmt of expr.body) {
1471
- if (stmt.kind === "ExprStmt")
1472
- retType = this.checkExpr(stmt.expr);
1473
- else
1474
- this.checkStmt(stmt);
1475
- }
1558
+ this.withComponentOrderNesting("lambda body", () => {
1559
+ for (const stmt of expr.body) {
1560
+ if (stmt.kind === "ExprStmt")
1561
+ retType = this.checkExpr(stmt.expr);
1562
+ else
1563
+ this.checkStmt(stmt);
1564
+ }
1565
+ });
1476
1566
  });
1477
1567
  const paramTypes = expr.params.length > 0
1478
1568
  ? expr.params.map((p) => (p.type ? this.resolveTypeRef(p.type) : exports.T_UNKNOWN))
@@ -1480,42 +1570,46 @@ class TypeChecker {
1480
1570
  return { tag: "func", params: paramTypes, ret: retType, suspend: false };
1481
1571
  }
1482
1572
  checkIfExpr(expr) {
1483
- this.checkExpr(expr.condition);
1484
- // Apply smart casts in the `then` branch
1485
- const narrowings = this.extractSmartCasts(expr.condition);
1486
- const thenScope = new Scope(this.scope);
1487
- for (const [name, type] of narrowings) {
1488
- const existing = this.scope.lookup(name);
1489
- if (existing)
1490
- thenScope.define({ ...existing, type });
1491
- }
1492
- const thenType = this.withScope(thenScope, () => expr.then.kind === "Block"
1493
- ? (this.checkBlock(expr.then), exports.T_UNIT)
1494
- : this.checkExpr(expr.then));
1495
- // Apply narrowings for the else branch (e.g. `x !is T` → else: x IS T)
1496
- const elseNarrowings = this.extractElseSmartCasts(expr.condition);
1497
- let elseType;
1498
- if (elseNarrowings.size > 0) {
1499
- const elseScope = new Scope(this.scope);
1500
- for (const [name, type] of elseNarrowings) {
1573
+ let resultType = exports.T_UNKNOWN;
1574
+ this.withComponentOrderNesting("if expression", () => {
1575
+ this.checkExpr(expr.condition);
1576
+ // Apply smart casts in the `then` branch
1577
+ const narrowings = this.extractSmartCasts(expr.condition);
1578
+ const thenScope = new Scope(this.scope);
1579
+ for (const [name, type] of narrowings) {
1501
1580
  const existing = this.scope.lookup(name);
1502
1581
  if (existing)
1503
- elseScope.define({ ...existing, type });
1582
+ thenScope.define({ ...existing, type });
1583
+ }
1584
+ const thenType = this.withScope(thenScope, () => expr.then.kind === "Block"
1585
+ ? (this.checkBlock(expr.then), exports.T_UNIT)
1586
+ : this.checkExpr(expr.then));
1587
+ // Apply narrowings for the else branch (e.g. `x !is T` → else: x IS T)
1588
+ const elseNarrowings = this.extractElseSmartCasts(expr.condition);
1589
+ let elseType;
1590
+ if (elseNarrowings.size > 0) {
1591
+ const elseScope = new Scope(this.scope);
1592
+ for (const [name, type] of elseNarrowings) {
1593
+ const existing = this.scope.lookup(name);
1594
+ if (existing)
1595
+ elseScope.define({ ...existing, type });
1596
+ }
1597
+ elseType = this.withScope(elseScope, () => expr.else.kind === "Block"
1598
+ ? (this.checkBlock(expr.else), exports.T_UNIT)
1599
+ : expr.else.kind === "IfExpr"
1600
+ ? this.checkIfExpr(expr.else)
1601
+ : this.checkExpr(expr.else));
1504
1602
  }
1505
- elseType = this.withScope(elseScope, () => expr.else.kind === "Block"
1506
- ? (this.checkBlock(expr.else), exports.T_UNIT)
1507
- : expr.else.kind === "IfExpr"
1508
- ? this.checkIfExpr(expr.else)
1509
- : this.checkExpr(expr.else));
1510
- }
1511
- else {
1512
- elseType = expr.else.kind === "Block"
1513
- ? (this.checkBlock(expr.else), exports.T_UNIT)
1514
- : expr.else.kind === "IfExpr"
1515
- ? this.checkIfExpr(expr.else)
1516
- : this.checkExpr(expr.else);
1517
- }
1518
- return this.unify([thenType, elseType]);
1603
+ else {
1604
+ elseType = expr.else.kind === "Block"
1605
+ ? (this.checkBlock(expr.else), exports.T_UNIT)
1606
+ : expr.else.kind === "IfExpr"
1607
+ ? this.checkIfExpr(expr.else)
1608
+ : this.checkExpr(expr.else);
1609
+ }
1610
+ resultType = this.unify([thenType, elseType]);
1611
+ });
1612
+ return resultType;
1519
1613
  }
1520
1614
  // ── Type resolution ────────────────────────────────────────────────────────
1521
1615
  lookupType(nameParts) {