Skip to content

Commit adb66c3

Browse files
author
Batiste Bieler
committed
Refactor
1 parent cff9668 commit adb66c3

9 files changed

Lines changed: 665 additions & 607 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// ============================================================================
2+
// Control Flow Handlers - Type inference for conditions (if/elseif)
3+
// ============================================================================
4+
5+
import { visit, pushToParent, visitChildren } from '../visitor.js';
6+
import { resolveTypeAlias } from '../typeSystem.js';
7+
import { AnyType } from '../Type.js';
8+
import { detectTypeofCheck, detectEqualityCheck, detectTruthinessCheck, detectPredicateGuard, applyIfBranchGuard, applyElseBranchGuard, applyPostIfGuard, detectImpossibleComparison } from '../typeGuards.js';
9+
import TypeChecker from '../typeChecker.js';
10+
import { getReturnTypeCount } from './shared.js';
11+
12+
/**
13+
* Create control flow handlers (condition and else_if)
14+
*/
15+
export function createControlFlowHandlers(getState) {
16+
return {
17+
condition: (node, parent) => {
18+
const { pushScope, popScope, lookupVariable, getCurrentScope, getFunctionScope, pushWarning, typeAliases } = getState();
19+
const functionScope = getFunctionScope();
20+
21+
// Check for impossible comparisons
22+
const impossibleComparison = detectImpossibleComparison(node.named.exp, lookupVariable, typeAliases);
23+
if (impossibleComparison) {
24+
const { variable, comparedValue, possibleValues } = impossibleComparison;
25+
pushWarning(
26+
node.named.exp,
27+
`This condition will always be false: '${variable}' has type ${possibleValues.join(' | ')} and can never equal ${comparedValue}`
28+
);
29+
}
30+
31+
const returnsBeforeIf = getReturnTypeCount(functionScope);
32+
33+
// Visit condition expression FIRST. Predicate call nodes need inferredType stamped
34+
// before detectPredicateGuard can identify them.
35+
if (node.named.exp) {
36+
visit(node.named.exp, node);
37+
}
38+
39+
// Detect type guard (syntax-based, then predicate which requires inferredType)
40+
const typeGuard = detectTypeofCheck(node.named.exp) || detectEqualityCheck(node.named.exp)
41+
|| detectTruthinessCheck(node.named.exp) || detectPredicateGuard(node.named.exp);
42+
43+
// Visit if branch (with type-narrowing scope when a type guard is present)
44+
if (typeGuard) {
45+
const ifScope = pushScope();
46+
applyIfBranchGuard(ifScope, typeGuard, lookupVariable, typeAliases);
47+
node.named.stats?.forEach(stat => visit(stat, node));
48+
popScope();
49+
} else {
50+
node.named.stats?.forEach(stat => visit(stat, node));
51+
}
52+
53+
const returnsAfterIf = getReturnTypeCount(functionScope);
54+
55+
// Visit else/elseif branch with type narrowing
56+
const elseNode = node.named.elseif;
57+
const isSimpleElse = elseNode && !elseNode.named?.exp && elseNode.named?.stats?.length > 0;
58+
59+
if (isSimpleElse) {
60+
if (typeGuard) {
61+
const elseScope = pushScope();
62+
applyElseBranchGuard(elseScope, typeGuard, lookupVariable, typeAliases);
63+
elseNode.named.stats.forEach(stat => visit(stat, node));
64+
popScope();
65+
} else {
66+
visit(elseNode, node);
67+
}
68+
} else if (elseNode) {
69+
if (typeGuard) {
70+
const elseifScope = pushScope();
71+
applyElseBranchGuard(elseifScope, typeGuard, lookupVariable, typeAliases);
72+
visit(elseNode, node);
73+
popScope();
74+
} else {
75+
visit(elseNode, node);
76+
}
77+
}
78+
79+
// When the if-branch is an early-exit type guard (always returns) with no else,
80+
// the code after this block is only reachable when the condition was false.
81+
// Apply type exclusion to the outer scope so subsequent statements see the narrowed type.
82+
// Note: the parser always creates an else_if node (matched with empty rule ['w?']) even
83+
// when there is no actual else clause, so we must check for meaningful else content.
84+
const ifBranchAlwaysReturns = returnsAfterIf > returnsBeforeIf;
85+
const elseHasContent = elseNode && (
86+
elseNode.named?.exp ||
87+
(elseNode.named?.stats && elseNode.named.stats.length > 0) ||
88+
elseNode.named?.elseif
89+
);
90+
if (typeGuard && !elseHasContent && ifBranchAlwaysReturns) {
91+
applyPostIfGuard(getCurrentScope(), typeGuard, lookupVariable, typeAliases);
92+
}
93+
94+
pushToParent(node, parent);
95+
},
96+
97+
else_if: (node, parent) => {
98+
const { pushScope, popScope, lookupVariable } = getState();
99+
100+
// Simple else branch: no condition, just visit body
101+
if (!node.named?.exp) {
102+
node.named?.stats?.forEach(stat => visit(stat, node));
103+
if (node.named?.elseif) {
104+
visit(node.named.elseif, node);
105+
}
106+
pushToParent(node, parent);
107+
return;
108+
}
109+
110+
// Visit the elseif condition expression
111+
visit(node.named.exp, node);
112+
113+
// Apply type narrowing for this elseif's own condition
114+
const typeGuard = detectTypeofCheck(node.named.exp) || detectEqualityCheck(node.named.exp)
115+
|| detectTruthinessCheck(node.named.exp) || detectPredicateGuard(node.named.exp);
116+
if (typeGuard) {
117+
const ifScope = pushScope();
118+
applyIfBranchGuard(ifScope, typeGuard, lookupVariable);
119+
node.named.stats?.forEach(stat => visit(stat, node));
120+
popScope();
121+
} else {
122+
node.named.stats?.forEach(stat => visit(stat, node));
123+
}
124+
125+
// Visit chained else_if, applying exclusion from this branch's typeGuard
126+
if (node.named.elseif) {
127+
if (typeGuard) {
128+
const elseScope = pushScope();
129+
applyElseBranchGuard(elseScope, typeGuard, lookupVariable);
130+
visit(node.named.elseif, node);
131+
popScope();
132+
} else {
133+
visit(node.named.elseif, node);
134+
}
135+
}
136+
137+
pushToParent(node, parent);
138+
},
139+
140+
assign_op: (node, parent) => {
141+
const { lookupVariable, pushWarning, typeAliases, inferencePhase } = getState();
142+
143+
// Always visit children so that expression inference is populated
144+
visitChildren(node);
145+
146+
// Type-checking only happens in the checking phase.
147+
// Also skip property-access variants (e.g. obj.prop += 1) — those
148+
// require resolving the property type from the object, which is handled
149+
// by the property-access validators elsewhere.
150+
if (inferencePhase !== 'checking' || node.named.target) {
151+
pushToParent(node, parent);
152+
return;
153+
}
154+
155+
const varName = node.named.name?.value;
156+
const expNode = node.named.exp;
157+
158+
if (!varName || !expNode) {
159+
pushToParent(node, parent);
160+
return;
161+
}
162+
163+
// Find the assign_operator token (e.g. '+=', '-=', '*=', '/=')
164+
const assignOperator = node.children?.find(c => c.type === 'assign_operator');
165+
if (!assignOperator) {
166+
pushToParent(node, parent);
167+
return;
168+
}
169+
170+
// Strip the trailing '=' to get the base arithmetic operator
171+
const opChar = assignOperator.value.slice(0, -1); // '+=' -> '+'
172+
173+
// Look up the variable's current type
174+
const varDef = lookupVariable(varName);
175+
if (!varDef || !varDef.type) {
176+
pushToParent(node, parent);
177+
return;
178+
}
179+
180+
const varType = resolveTypeAlias(varDef.type, typeAliases);
181+
const rhsType = expNode.inference?.[0];
182+
183+
if (!rhsType || rhsType === AnyType || varType === AnyType) {
184+
pushToParent(node, parent);
185+
return;
186+
}
187+
188+
// Validate the compound assignment using the same math-operation rules
189+
const result = TypeChecker.checkMathOperation(varType, rhsType, opChar);
190+
if (!result.valid) {
191+
const msgs = result.warning ? [result.warning] : (result.warnings ?? []);
192+
msgs.forEach(msg => pushWarning(node, msg));
193+
}
194+
195+
pushToParent(node, parent);
196+
},
197+
};
198+
}

src/inference/handlers/expressions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Expression Handlers - Type inference for expressions
33
// ============================================================================
44

5-
import { visit, visitChildren, resolveTypes, pushToParent, validateObjectPropertyAccess } from '../visitor.js';
5+
import { visit, visitChildren, resolveTypes, pushToParent, validateObjectPropertyAccess, stampInferencePhaseOnly } from '../visitor.js';
66
import { inferGenericArguments, substituteType, resolveTypeAlias, createUnionType, removeNullish, isUnionType, parseUnionType, getBaseTypeOfLiteral, isTypeCompatible } from '../typeSystem.js';
77
import { parseTypeExpression } from '../typeParser.js';
88
import { ObjectType, PrimitiveType, AnyType, ArrayType, FunctionType, AnyFunctionType, UndefinedType, TypeAlias, GenericType, StringType, NumberType, BooleanType, NullType, NeverType, PredicateType, LiteralType, TupleType, Types } from '../Type.js';

src/inference/handlers/functions.js

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Function Handlers - Type inference for function definitions and calls
33
// ============================================================================
44

5-
import { visitChildren } from '../visitor.js';
5+
import { visitChildren, stampInferencePhaseOnly } from '../visitor.js';
66
import {
77
getAnnotationType,
88
isTypeCompatible,
@@ -281,13 +281,11 @@ function finalizeFunctionReturnType({
281281
if (checkTarget && inferredType !== AnyType && !isTypeCompatible(inferredType, checkTarget, typeAliases)) {
282282
pushWarning(nameNode, `${warningLabel} returns ${getBaseTypeOfLiteral(inferredType)} but declared as ${declaredType}`);
283283
}
284-
if (inferencePhase === 'inference' && nameNode?.inferredType === undefined) {
285-
nameNode.inferredType = new FunctionType(
286-
scope.__currentFctParams, declaredType ?? inferredType,
287-
genericParams, scope.__currentFctParamNames, null,
288-
genericConstraints?.size > 0 ? genericConstraints : null
289-
);
290-
}
284+
stampInferencePhaseOnly(nameNode, new FunctionType(
285+
scope.__currentFctParams, declaredType ?? inferredType,
286+
genericParams, scope.__currentFctParamNames, null,
287+
genericConstraints?.size > 0 ? genericConstraints : null
288+
));
291289
return { declaredType, inferredType };
292290
}
293291

@@ -356,9 +354,7 @@ function createFunctionHandlers(getState) {
356354
}
357355

358356
scope[localName] = { type: propType };
359-
if (inferencePhase === 'inference') {
360-
token.inferredType = resolveTypeAlias(propType, typeAliases);
361-
}
357+
stampInferencePhaseOnly(token, resolveTypeAlias(propType, typeAliases));
362358
regDestrName(v.named.more);
363359
}
364360

@@ -378,9 +374,7 @@ function createFunctionHandlers(getState) {
378374

379375
scope.__currentFctParams.push(paramType);
380376
scope.__currentFctParamNames.push(node.named.name.value);
381-
if (inferencePhase === 'inference' && node.named.name) {
382-
node.named.name.inferredType = resolveTypeAlias(paramType, typeAliases);
383-
}
377+
stampInferencePhaseOnly(node.named.name, resolveTypeAlias(paramType, typeAliases));
384378
visitChildren(node);
385379
}
386380
},
@@ -462,12 +456,12 @@ function createFunctionHandlers(getState) {
462456
? wrapInPromise(declaredType ?? inferredType)
463457
: (declaredType ?? inferredType);
464458
// Re-stamp hover type with the Promise-wrapped return type.
465-
if (isAsync && inferencePhase === 'inference') {
466-
node.named.name.inferredType = new FunctionType(
459+
if (isAsync) {
460+
stampInferencePhaseOnly(node.named.name, new FunctionType(
467461
scope.__currentFctParams, externalReturnType,
468462
genericParams, scope.__currentFctParamNames, null,
469463
genericConstraints?.size > 0 ? genericConstraints : null
470-
);
464+
));
471465
}
472466
parentScope[node.named.name.value] = {
473467
source: 'func_def',
@@ -550,9 +544,7 @@ function createFunctionHandlers(getState) {
550544
type: classType,
551545
node,
552546
};
553-
if (inferencePhase === 'inference' && node.named.name.inferredType === undefined) {
554-
node.named.name.inferredType = classType;
555-
}
547+
stampInferencePhaseOnly(node.named.name, classType);
556548
}
557549

558550
// Push a scope for the class body so __currentClassType is scoped
@@ -600,13 +592,13 @@ function createFunctionHandlers(getState) {
600592
});
601593

602594
// Async methods expose Promise<T>; re-stamp the hover type accordingly.
603-
if (node.named.async && inferencePhase === 'inference' && node.named.name) {
595+
if (node.named.async) {
604596
const externalReturnType = wrapInPromise(methodDeclaredType ?? methodInferredType);
605-
node.named.name.inferredType = new FunctionType(
597+
stampInferencePhaseOnly(node.named.name, new FunctionType(
606598
scope.__currentFctParams, externalReturnType,
607599
genericParams, scope.__currentFctParamNames, null,
608600
genericConstraints?.size > 0 ? genericConstraints : null
609-
);
601+
));
610602
}
611603

612604
popScope();
@@ -622,9 +614,7 @@ function createFunctionHandlers(getState) {
622614

623615
stampTypeAnnotation(annotation);
624616
const memberType = getAnnotationType(annotation);
625-
if (memberType && inferencePhase === 'inference' && node.named.name?.inferredType === undefined) {
626-
node.named.name.inferredType = memberType;
627-
}
617+
stampInferencePhaseOnly(node.named.name, memberType);
628618
},
629619
};
630620
}

0 commit comments

Comments
 (0)