Skip to content

Commit 600465f

Browse files
antonsyndclaude
andcommitted
refactor(semantic): every expectation push goes through EnterStore — 26 raw _expectedType writes migrated
StoreContext records position/slot/callee/ordinal (no consumer yet). ClearExpectation handles the 7 null-push sites. StoreSeamConformanceTests.ExpectationIsPushedOnlyThroughEnterStore scans for raw writes (0) and anchors the call-site count to 38. mutation: raw write reintroduced → red (1 failed), restored → green Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017WDHWSPvShmKRoGytd9ZXw
1 parent 646b9cf commit 600465f

7 files changed

Lines changed: 245 additions & 108 deletions

src/Sharpy.Compiler.Tests/Semantic/StoreSeamConformanceTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,88 @@ public void Scan_FindsTheKnownCallSites()
173173
"IsAssignable is the one place a data-level call belongs");
174174
}
175175

176+
/// <summary>
177+
/// Every <c>_expectedType =</c> write in the checker must go through <c>EnterStore</c> or
178+
/// <c>ClearExpectation</c> (plan-ebd58b Phase 1). A raw write bypasses the save/restore seam
179+
/// and the <c>StoreContext</c> record, which is the defect class Phase 2 reads to compose
180+
/// diagnostic context.
181+
/// </summary>
182+
[Fact]
183+
public void ExpectationIsPushedOnlyThroughEnterStore()
184+
{
185+
var semanticDir = FindCompilerSemanticDirectory();
186+
var files = Directory.GetFiles(semanticDir, "TypeChecker*.cs", SearchOption.TopDirectoryOnly);
187+
188+
files.Should().NotBeEmpty("positive control: the scan must find TypeChecker files");
189+
190+
var violations = new List<string>();
191+
var enterStoreCount = 0;
192+
var clearExpectationCount = 0;
193+
var scannedFileCount = 0;
194+
195+
foreach (var file in files)
196+
{
197+
var fileName = Path.GetFileName(file);
198+
199+
// The seam itself is the ONE file allowed to write _expectedType
200+
if (fileName == "TypeChecker.StoreConversion.cs")
201+
continue;
202+
203+
scannedFileCount++;
204+
var text = File.ReadAllText(file);
205+
var lines = text.Split('\n');
206+
207+
for (int i = 0; i < lines.Length; i++)
208+
{
209+
var line = lines[i];
210+
var trimmed = line.TrimStart();
211+
212+
// Skip comments
213+
if (trimmed.StartsWith("//") || trimmed.StartsWith("*") || trimmed.StartsWith("///"))
214+
continue;
215+
216+
// Field declaration is allowed: `private SemanticType? _expectedType = null;`
217+
if (trimmed.Contains("private") && trimmed.Contains("SemanticType?") && trimmed.Contains("_expectedType"))
218+
continue;
219+
220+
// Check for raw `_expectedType =` writes (assignment, not comparison)
221+
if (System.Text.RegularExpressions.Regex.IsMatch(line, @"_expectedType\s*=[^=]"))
222+
violations.Add($"{fileName}:{i + 1}: {trimmed.TrimEnd()}");
223+
224+
// Check for `ref _expectedType` (ScopedValue.Push)
225+
if (line.Contains("ref _expectedType"))
226+
violations.Add($"{fileName}:{i + 1}: {trimmed.TrimEnd()}");
227+
}
228+
229+
// Count EnterStore and ClearExpectation call sites in this file
230+
enterStoreCount += System.Text.RegularExpressions.Regex.Matches(text, @"EnterStore\(").Count;
231+
clearExpectationCount += System.Text.RegularExpressions.Regex.Matches(text, @"ClearExpectation\(").Count;
232+
}
233+
234+
// Also count EnterStore/ClearExpectation in StoreConversion.cs (only the definition,
235+
// not call sites — but the definition includes the method name)
236+
var storeConversionText = File.ReadAllText(Path.Combine(semanticDir, "TypeChecker.StoreConversion.cs"));
237+
enterStoreCount += System.Text.RegularExpressions.Regex.Matches(storeConversionText, @"EnterStore\(").Count;
238+
// Subtract 1 for the definition itself
239+
enterStoreCount -= 1;
240+
clearExpectationCount += System.Text.RegularExpressions.Regex.Matches(storeConversionText, @"ClearExpectation\(").Count;
241+
// Subtract 1 for the definition itself
242+
clearExpectationCount -= 1;
243+
244+
violations.Should().BeEmpty(
245+
"every _expectedType write must go through EnterStore or ClearExpectation — "
246+
+ "a raw write bypasses the save/restore seam and the StoreContext record. Found: "
247+
+ string.Join("; ", violations));
248+
249+
scannedFileCount.Should().BeGreaterThan(0,
250+
"positive control: the scan must examine at least one TypeChecker file");
251+
252+
var totalCallSites = enterStoreCount + clearExpectationCount;
253+
totalCallSites.Should().Be(38,
254+
"the literal anchor for EnterStore + ClearExpectation call sites "
255+
+ $"(got {enterStoreCount} EnterStore + {clearExpectationCount} ClearExpectation = {totalCallSites})");
256+
}
257+
176258
private record CallSite(string File, string Method, int Line, string Text)
177259
{
178260
public string Key => $"{File}::{Method}";

src/Sharpy.Compiler/Semantic/TypeChecker.Expressions.Access.Calls.cs

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3800,10 +3800,9 @@ private SemanticType CheckIifeLambdaCall(FunctionCall call, LambdaExpression lam
38003800
};
38013801

38023802
// 4. Check lambda with expected type context
3803-
var saved = _expectedType;
3804-
_expectedType = expectedFuncType;
3805-
var lambdaType = CheckExpression(call.Function);
3806-
_expectedType = saved;
3803+
SemanticType lambdaType;
3804+
using (EnterStore(StorePosition.ArgumentPositional, expectedFuncType, call.Function))
3805+
lambdaType = CheckExpression(call.Function);
38073806

38083807
// 5. Return the inferred return type
38093808
return lambdaType is FunctionType ft ? ft.ReturnType : SemanticType.Unknown;
@@ -4079,7 +4078,6 @@ private SemanticType CheckUnionCaseConstruction(
40794078
{
40804079
for (int argIdx = 0; argIdx < call.Arguments.Length; argIdx++)
40814080
{
4082-
var previousExpectedType = _expectedType;
40834081
var previousParameterTypedArgument = _parameterTypedArgument;
40844082

40854083
// Cleared up front, so the arms below can only ever set it TOGETHER with the
@@ -4107,7 +4105,6 @@ private SemanticType CheckUnionCaseConstruction(
41074105
else
41084106
argTypes.Add(SemanticType.Unknown);
41094107
}
4110-
_expectedType = previousExpectedType;
41114108
_parameterTypedArgument = previousParameterTypedArgument;
41124109
continue;
41134110
}
@@ -4119,24 +4116,27 @@ private SemanticType CheckUnionCaseConstruction(
41194116
&& earlyFuncSymbol != null && argIdx + earlyParamOffset < earlyFuncSymbol.Parameters.Count)
41204117
{
41214118
var paramType = earlyFuncSymbol.Parameters[argIdx + earlyParamOffset].Type;
4122-
_expectedType = paramType is UnknownType ? null : paramType;
4123-
_parameterTypedArgument = ParameterTypedArgumentOf(paramType, call.Arguments[argIdx]);
4119+
using (EnterStore(StorePosition.ArgumentPositional, paramType, call.Arguments[argIdx]))
4120+
argTypes.Add(CheckExpression(call.Arguments[argIdx]));
41244121
}
41254122
else if (!noCandidateExpectation
41264123
&& calleeFunctionType != null && argIdx < calleeFunctionType.ParameterTypes.Count)
41274124
{
41284125
var paramType = calleeFunctionType.ParameterTypes[argIdx];
4129-
_expectedType = paramType is UnknownType ? null : paramType;
4130-
_parameterTypedArgument = ParameterTypedArgumentOf(paramType, call.Arguments[argIdx]);
4126+
using (EnterStore(StorePosition.ArgumentPositional, paramType, call.Arguments[argIdx]))
4127+
argTypes.Add(CheckExpression(call.Arguments[argIdx]));
41314128
}
41324129
else if (noCandidateExpectation)
41334130
{
41344131
// The ENCLOSING context's expectation is not this argument's parameter type
41354132
// either, and leaving it in place would type the literal from it.
4136-
_expectedType = null;
4133+
using (ClearExpectation(call.Arguments[argIdx]))
4134+
argTypes.Add(CheckExpression(call.Arguments[argIdx]));
4135+
}
4136+
else
4137+
{
4138+
argTypes.Add(CheckExpression(call.Arguments[argIdx]));
41374139
}
4138-
argTypes.Add(CheckExpression(call.Arguments[argIdx]));
4139-
_expectedType = previousExpectedType;
41404140
_parameterTypedArgument = previousParameterTypedArgument;
41414141
}
41424142
}
@@ -4158,24 +4158,27 @@ private SemanticType CheckUnionCaseConstruction(
41584158
span: kwarg.Span ?? kwarg.Value.Span);
41594159
}
41604160

4161-
var previousExpectedType = _expectedType;
41624161
var previousParameterTypedArgument = _parameterTypedArgument;
41634162
_parameterTypedArgument = null;
4164-
if (calleeDenotesOverloadSet && TakesContextualCollectionType(kwarg.Value))
4163+
IDisposable? kwScope = null;
4164+
try
41654165
{
4166-
_expectedType = null;
4167-
}
4168-
else if (earlyFuncSymbol != null)
4169-
{
4170-
var param = FindKeywordParameter(earlyFuncSymbol.Parameters, kwarg.Name);
4171-
if (param != null)
4166+
if (calleeDenotesOverloadSet && TakesContextualCollectionType(kwarg.Value))
41724167
{
4173-
_expectedType = param.Type is UnknownType ? null : param.Type;
4174-
_parameterTypedArgument = ParameterTypedArgumentOf(param.Type, kwarg.Value);
4168+
kwScope = ClearExpectation(kwarg.Value);
41754169
}
4170+
else if (earlyFuncSymbol != null)
4171+
{
4172+
var param = FindKeywordParameter(earlyFuncSymbol.Parameters, kwarg.Name);
4173+
if (param != null)
4174+
kwScope = EnterStore(StorePosition.ArgumentKeyword, param.Type, kwarg.Value, keywordName: kwarg.Name);
4175+
}
4176+
kwargTypes[kwarg.Name] = CheckExpression(kwarg.Value);
4177+
}
4178+
finally
4179+
{
4180+
kwScope?.Dispose();
41764181
}
4177-
kwargTypes[kwarg.Name] = CheckExpression(kwarg.Value);
4178-
_expectedType = previousExpectedType;
41794182
_parameterTypedArgument = previousParameterTypedArgument;
41804183
}
41814184

@@ -4229,26 +4232,28 @@ private bool TryCheckMapLambdaArguments(FunctionCall call, Expression callee, Li
42294232
// that CheckLambda ignores (it maps only up to the lambda's own arity).
42304233
var elementTypes = new List<SemanticType>();
42314234
var iterableTypes = new List<SemanticType>();
4232-
var previousExpectedType = _expectedType;
4233-
_expectedType = null;
4234-
for (int i = 1; i < call.Arguments.Length; i++)
4235+
using (ClearExpectation(null))
42354236
{
4236-
var argType = CheckExpression(call.Arguments[i]);
4237-
iterableTypes.Add(argType);
4238-
var elem = _typeInference.InferIterableElementType(argType);
4239-
elementTypes.Add(elem ?? SemanticType.Unknown);
4237+
for (int i = 1; i < call.Arguments.Length; i++)
4238+
{
4239+
var argType = CheckExpression(call.Arguments[i]);
4240+
iterableTypes.Add(argType);
4241+
var elem = _typeInference.InferIterableElementType(argType);
4242+
elementTypes.Add(elem ?? SemanticType.Unknown);
4243+
}
42404244
}
42414245

42424246
// Feed the synthesized expected function type into the lambda so CheckLambda types its
42434247
// parameters from the element types (lambdas/CheckLambda already consume a FunctionType
42444248
// _expectedType). The return type is left Unknown; the body determines it.
4245-
_expectedType = new FunctionType
4249+
var expectedMapFuncType = new FunctionType
42464250
{
42474251
ParameterTypes = elementTypes,
42484252
ReturnType = SemanticType.Unknown
42494253
};
4250-
var lambdaType = CheckExpression(call.Arguments[0]);
4251-
_expectedType = previousExpectedType;
4254+
SemanticType lambdaType;
4255+
using (EnterStore(StorePosition.ArgumentPositional, expectedMapFuncType, call.Arguments[0]))
4256+
lambdaType = CheckExpression(call.Arguments[0]);
42524257

42534258
// Reassemble positional argTypes in source order: [lambda, iter1, iter2, ...].
42544259
argTypes.Add(lambdaType);

src/Sharpy.Compiler/Semantic/TypeChecker.Expressions.Access.Lambdas.cs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,16 @@ private bool TryCheckDeferredLambdaArguments(
282282
{
283283
if (deferredPositions.Contains(position))
284284
continue;
285+
SemanticType actual;
285286
if (providesExpectedTypes && formalByPosition[position] is { } positionalFormal)
286-
_expectedType = positionalFormal is UnknownType ? null : positionalFormal;
287-
var actual = CheckExpression(call.Arguments[position]);
288-
_expectedType = previousExpectedType;
287+
{
288+
using (EnterStore(StorePosition.LambdaParameterDefault, positionalFormal, call.Arguments[position]))
289+
actual = CheckExpression(call.Arguments[position]);
290+
}
291+
else
292+
{
293+
actual = CheckExpression(call.Arguments[position]);
294+
}
289295
positionTypes[position] = actual;
290296
if (formalByPosition[position] is { } formal)
291297
{
@@ -298,10 +304,16 @@ private bool TryCheckDeferredLambdaArguments(
298304
{
299305
if (deferredKeywords.Contains(kwarg.Name))
300306
continue;
307+
SemanticType actual;
301308
if (providesExpectedTypes && formalByKeyword.TryGetValue(kwarg.Name, out var keywordFormal))
302-
_expectedType = keywordFormal is UnknownType ? null : keywordFormal;
303-
var actual = CheckExpression(kwarg.Value);
304-
_expectedType = previousExpectedType;
309+
{
310+
using (EnterStore(StorePosition.LambdaParameterDefault, keywordFormal, kwarg.Value))
311+
actual = CheckExpression(kwarg.Value);
312+
}
313+
else
314+
{
315+
actual = CheckExpression(kwarg.Value);
316+
}
305317
kwargTypes[kwarg.Name] = actual;
306318
if (formalByKeyword.TryGetValue(kwarg.Name, out var boundFormal))
307319
{
@@ -344,8 +356,9 @@ private bool TryCheckDeferredLambdaArguments(
344356
{
345357
var formal = formalByPosition[position]!;
346358
SemanticType checkedType;
347-
using (ScopedValue.Push(ref _expectedType,
348-
SubstituteExpectedLambdaType(formal, substitutions) ?? previousExpectedType))
359+
using (EnterStore(StorePosition.LambdaBody,
360+
SubstituteExpectedLambdaType(formal, substitutions) ?? previousExpectedType ?? SemanticType.Unknown,
361+
call.Arguments[position]))
349362
{
350363
checkedType = CheckExpression(call.Arguments[position]);
351364
}
@@ -359,8 +372,9 @@ private bool TryCheckDeferredLambdaArguments(
359372

360373
var keywordFormal = formalByKeyword[kwarg.Name];
361374
SemanticType checkedType;
362-
using (ScopedValue.Push(ref _expectedType,
363-
SubstituteExpectedLambdaType(keywordFormal, substitutions) ?? previousExpectedType))
375+
using (EnterStore(StorePosition.LambdaBody,
376+
SubstituteExpectedLambdaType(keywordFormal, substitutions) ?? previousExpectedType ?? SemanticType.Unknown,
377+
kwarg.Value))
364378
{
365379
checkedType = CheckExpression(kwarg.Value);
366380
}

0 commit comments

Comments
 (0)