Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions build_tools/tests/test_allowlist_issue_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,29 @@ def test_knownred_cell_citing_a_non_open_issue_is_offence(tmp_path, state):
with pytest.raises(SystemExit) as exc:
mod.main()
assert exc.value.code == 1


@pytest.mark.parametrize("state", ["OPEN", "MERGED", "CLOSED"])
def test_query_states_maps_a_pull_request_url_to_pull_request(state):
"""`gh issue view` answers for a pull request too; query_states must name it PULL_REQUEST, so an
OPEN pull request cited as a tracker is red, not read as an open issue (#2288 verify)."""
def fake_run(args, **kwargs):
if args[:2] == ["gh", "--version"]:
return subprocess.CompletedProcess(args, 0, stdout="gh version 2\n", stderr="")
return subprocess.CompletedProcess(
args, 0, stdout=f"{state} https://github.com/antonsynd/sharpy/pull/1\n", stderr="")

with patch.object(mod.subprocess, "run", fake_run):
assert mod.query_states([1]) == {1: "PULL_REQUEST"}


def test_query_states_keeps_an_issue_urls_state():
"""The positive control: an issue url keeps its own state."""
def fake_run(args, **kwargs):
if args[:2] == ["gh", "--version"]:
return subprocess.CompletedProcess(args, 0, stdout="gh version 2\n", stderr="")
return subprocess.CompletedProcess(
args, 0, stdout="OPEN https://github.com/antonsynd/sharpy/issues/2287\n", stderr="")

with patch.object(mod.subprocess, "run", fake_run):
assert mod.query_states([2287]) == {2287: "OPEN"}
3 changes: 0 additions & 3 deletions src/Sharpy.Compiler.Tests/Infrastructure/AstDescendants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,4 @@ public static IEnumerable<Node> SelfAndDescendants(Node root)
stack.Push(children[i]);
}
}

/// <summary>Every node below <paramref name="root"/>, excluding the root itself.</summary>
public static IEnumerable<Node> Descendants(Node root) => SelfAndDescendants(root).Skip(1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ or Verdict.SharpyOkPythonError
Output.WriteLine($" DIVERGENCE [{o.Verdict}] {o.Program.Key}: {o.Detail}");

// Enumeration sanity — a corpus that shrank to nothing is itself a regression.
Assert.True(results.Count > 0, "No programs were compared.");
Assert.True(results.Count == programs.Count, "Not every program was classified.");

// Per-module vacuity guard. Admitting a module to CPythonAvailableModules only gets its
// fixtures PAST THE FILTER; it does not make them comparable. If CPython cannot import the
Expand Down Expand Up @@ -337,7 +337,7 @@ private Corpus BuildCorpus(int limit)
if (limit <= FullPool)
{
var full = Fixtures(int.MaxValue);
return Compose(handpicked, full, Generated(FullPoolGeneratedTarget));
return Compose(handpicked, full, Generated(FullPoolGeneratedTarget).Programs);
}

// DIFF_EXEC_LIMIT=<n>: a subsample for fast local iteration. Split the remaining budget
Expand All @@ -349,7 +349,7 @@ private Corpus BuildCorpus(int limit)
var pool = Fixtures(fixtureTarget);
// Any fixture budget the pool could not fill rolls over to the generated arm.
generatedTarget += Math.Max(0, fixtureTarget - pool.Selected.Count);
return Compose(handpicked, pool, Generated(generatedTarget));
return Compose(handpicked, pool, Generated(generatedTarget).Programs);
}

private static Corpus Compose(List<Program> handpicked, FixturePool pool, List<Program> generated)
Expand Down Expand Up @@ -987,12 +987,22 @@ void Exclude(string shape, string? name = null)
/// <c>print(expr)</c> statements over int / str / bool / list results with fuel 1 (shallow enough
/// that int arithmetic cannot overflow Sharpy's 32-bit <c>int</c> and masquerade as a divergence).
/// Each candidate must compile in Sharpy and pass the subset filter.
/// <para>Nothing is dropped silently (#2288): a candidate Sharpy refuses or throws on is recorded on
/// a <see cref="RejectionSink"/> through <see cref="SemanticFilter.AdmitWellTyped"/> (the generated
/// lane's one admission rule), an admitted candidate whose re-parse fails or throws is a recorded
/// failure, and only the subset filter — a property of the ORACLE, not of the compiler — discards a
/// sample. <see cref="AssertGeneratedArmSound"/> turns any of them into a red, and the arm must reach
/// its target. Before, every one of those samples was a bare <c>return</c> and the 12-program target
/// was never checked.</para>
/// </summary>
private static List<Program> Generated(int target)
private static GeneratedArm Generated(int target)
{
var results = new List<Program>();
var sink = new RejectionSink("DifferentialExecution.Generated");
var failures = new List<string>();
int outsideSubset = 0;
if (target <= 0)
return results;
return new GeneratedArm(results, target, sink, failures, outsideSubset);

var env = TypeEnv.WithCollections;
var types = new[] { "int", "str", "bool", "list[int]", "list[str]" };
Expand Down Expand Up @@ -1020,39 +1030,84 @@ select BuildGeneratedMain(xv, yv, sv, flagv,
if (results.Count >= target)
return;

string source;
try
{ source = Sharpy.Compiler.Pretty.Unparser.Unparse(module); }
catch { return; }
// Gate 1: Sharpy accepts it (analysis-clean) — or the sink records why not.
if (!SemanticFilter.AdmitWellTyped(module, sink))
return;

var source = Sharpy.Compiler.Pretty.Unparser.Unparse(module);
if (!seenSources.Add(source))
return;

// Gate 1: Sharpy accepts it (analysis-clean).
// Gate 2: whole tree is in the shared executable subset (reparse then filter). The program
// was just analysed clean, so a re-parse error or throw is a failure, not a skip.
Module reparsed;
try
{
var analysis = new Sharpy.Compiler.Compiler().Analyze(source, "gen_diff.spy");
if (!analysis.Success)
var parser = new Sharpy.Compiler.Parser.Parser(
new Sharpy.Compiler.Lexer.Lexer(source).TokenizeAll());
reparsed = parser.ParseModule();
var parseErrors = parser.Diagnostics.GetErrors();
if (parseErrors.Count > 0)
{
failures.Add($"an admitted program re-parses with [{string.Join(", ", parseErrors.Select(d => d.Code))}]:\n{source}");
return;
}
}
catch (Exception e)
{
failures.Add($"re-parsing an admitted program threw {e.GetType().Name}:\n{source}\n{e}");
return;
}
catch { return; }

// Gate 2: whole tree is in the shared executable subset (reparse then filter).
try
if (!ExecSubsetFilter.IsInSharedSubset(reparsed))
{
var parser = new Sharpy.Compiler.Parser.Parser(
new Sharpy.Compiler.Lexer.Lexer(source).TokenizeAll());
var reparsed = parser.ParseModule();
if (parser.Diagnostics.HasErrors || !ExecSubsetFilter.IsInSharedSubset(reparsed))
return;
outsideSubset++;
return;
}
catch { return; }

results.Add(new Program($"generated::g{idx++:D4}", source));
// threads:1 keeps this collection loop single-threaded (results/seenSources are not
// threads:1 keeps this collection loop single-threaded (results/seenSources/failures are not
// concurrent); seed pins determinism.
}, seed: GeneratedSeed, iter: Math.Max(target * 12L, 96L), threads: 1);

return results;
var arm = new GeneratedArm(results, target, sink, failures, outsideSubset);
AssertGeneratedArmSound(arm);
return arm;
}

/// <summary>The generated arm's programs and what it discarded on the way (#2288).</summary>
private sealed record GeneratedArm(
List<Program> Programs, int Target, RejectionSink Sink, List<string> Failures, int OutsideSubset)
{
public string Summary =>
$"{Sink.Summary}; re-parse failures {Failures.Count}; outside the shared subset {OutsideSubset}; "
+ $"programs {Programs.Count}/{Target}";
}

/// <summary>The generated arm dropped nothing silently and reached its target: no unexplained
/// rejection on the sink, no re-parse failure, and <see cref="GeneratedArm.Target"/> programs.</summary>
private static void AssertGeneratedArmSound(GeneratedArm arm)
{
Assert.True(arm.Sink.Unexplained.Count == 0, arm.Sink.Report());
Assert.True(arm.Failures.Count == 0,
$"{arm.Failures.Count} admitted generated program(s) failed to re-parse; the first:\n{arm.Failures.FirstOrDefault()}");
Assert.True(arm.Programs.Count == arm.Target,
$"The generated arm reached {arm.Programs.Count}/{arm.Target} programs ({arm.Summary}) — a sweep "
+ "whose fuzz arm falls short compares fewer programs than it claims");
}

/// <summary>
/// The generated arm at its full-pool size, without the CPython oracle (the sweep's fact returns
/// early when <c>python3</c> is absent, so this is the arm's guard on every machine): every candidate
/// is admitted, pinned, or outside the shared subset, and the arm reaches
/// <see cref="FullPoolGeneratedTarget"/> programs.
/// </summary>
[Fact]
public void GeneratedArm_DropsNothingSilently_AndReachesItsTarget()
{
var arm = Generated(FullPoolGeneratedTarget);
Output.WriteLine(arm.Summary);
Assert.Equal(FullPoolGeneratedTarget, arm.Programs.Count);
}

// ----------------------------------------------------------------------------------------- //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
/// <list type="bullet">
/// <item><c>Error</c>: the distinct error codes are exactly [<paramref name="expectedCode"/>]
/// (warnings are not read). Pinned: no error at all.</item>
/// <item><c>Warning</c>: the distinct warning codes are exactly [<paramref name="expectedCode"/>]
/// AND there is no error — a warning lane whose program is also refused proves nothing about the
/// warning. Pinned: no error, and no warning of the code (no warning at all when the code is
/// null).</item>
/// <item><c>Warning</c>: exactly ONE warning DIAGNOSTIC, carrying <paramref name="expectedCode"/>, AND no
/// error — a warning lane whose program is also refused proves nothing about the warning, and two
/// warnings of the code are two broken facts, not the one the injection made (P63 Design Decision
/// 6: "exactly one diagnostic of the expected severity"). Pinned: no error, and no warning of the
/// code (no warning at all when the code is null).</item>
/// </list>
/// Expected codes are literal strings at the call site, never <c>DiagnosticCodes</c> constants —
/// a renumbering mutation must stay live.
Expand Down Expand Up @@ -100,11 +101,13 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
var source = sourceForMessage;
List<string> errors;
List<string> warnings;
int warningDiagnostics;
try
{
var (success, all) = analyse();
errors = DistinctCodes(all.Where(d => d.IsError));
warnings = DistinctCodes(all.Where(d => d.IsWarning));
warningDiagnostics = all.Count(d => d.IsWarning);
if (!success && errors.Count == 0)
return $"Injected {label} failed analysis without an error code:\n{source}";
}
Expand All @@ -116,7 +119,9 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
var isWarning = severity == CompilerDiagnosticSeverity.Warning;
// The arm's own codes: errors on the error arm, warnings on the warning arm.
var drew = Describe(isWarning ? warnings : errors);
var drewAll = isWarning ? $"errors [{Describe(errors)}], warnings [{drew}]" : $"[{drew}]";
var drewAll = isWarning
? $"errors [{Describe(errors)}], warnings [{drew}] ({warningDiagnostics} warning diagnostic(s))"
: $"[{drew}]";

if (pin is not null)
{
Expand All @@ -135,9 +140,9 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
?? throw new InvalidOperationException($"Injection {label} has neither an expected code nor a KnownRed pin");
if (isWarning)
{
return warnings is [var onlyWarning] && onlyWarning == expected && errors.Count == 0
return warningDiagnostics == 1 && warnings is [var onlyWarning] && onlyWarning == expected && errors.Count == 0
? null
: $"Injected {label} must draw exactly warning {expected} and no error, drew {drewAll}:\n{source}";
: $"Injected {label} must draw exactly one warning, {expected}, and no error, drew {drewAll}:\n{source}";
}

return errors is [var only] && only == expected
Expand Down Expand Up @@ -170,6 +175,36 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
}
}

/// <summary>
/// The no-crash verdict for a RAW sample — the one a <c>*NeverThrows*</c> lane reads (#2288):
/// null when analysing <paramref name="source"/> neither throws nor records an internal compiler
/// error (SPY0909, the code the compiler's last-chance handlers turn an escaped exception into),
/// otherwise why not. A refusal is NOT a failure here — the sample is unfiltered — but no throw is
/// ever acceptable: before, the lanes caught only <c>InternalCompilerErrorException</c> (which
/// <c>Analyze</c> never lets escape: it records SPY0909) and swallowed every other exception, so
/// they could not fail.
/// </summary>
public static string? JudgeNoInternalError(string source, string label)
{
try
{
var (_, all) = AnalyseSingleFile(source);
var internalErrors = all.Where(d => d.Code == InternalCompilerErrorCode).ToList();
return internalErrors.Count == 0
? null
: $"Raw {label} drew an internal compiler error ({InternalCompilerErrorCode}): "
+ $"{internalErrors[0].Message}\n{source}";
}
catch (Exception e)
{
return $"Raw {label} threw {e.GetType().Name} — the compiler must never throw:\n{source}\n{e}";
}
}

/// <summary><c>DiagnosticCodes.Infrastructure.InternalCompilerError</c>, spelled literally (a
/// renumbering must stay a live mutation).</summary>
private const string InternalCompilerErrorCode = "SPY0909";

/// <summary>What <see cref="JudgeSamples"/> saw: <see cref="Tested"/> samples judged,
/// <see cref="Failures"/> non-null verdicts, <see cref="First"/> the first of them.</summary>
public sealed record SampleVerdicts(int Tested, int Failures, string? First);
Expand Down Expand Up @@ -689,7 +724,7 @@ private static Expression ReplaceNestedCall(Expression expr, FunctionCall target
return null;
}

private static readonly KnownRedRefusal ExplicitInstantiationConstraintPin = new("#2212",
private static readonly KnownRedRefusal ExplicitInstantiationConstraintPin = new KnownRedRefusal("#2212",
"analyses clean; run is SPY0908 CS0311 — a violated constraint at an explicit instantiation is refused by Roslyn, not Sharpy");

private const string PlainClassName = "Plain";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static Gen<Module> WellTypedProgram(Gen<Module> source, RejectionSink sin
{
try
{
return Admit(sink, () => Sharpy.Compiler.Pretty.Unparser.Unparse(module), module, AnalyseSingleFile);
return AdmitWellTyped(module, sink);
}
finally
{
Expand All @@ -35,6 +35,15 @@ public static Gen<Module> WellTypedProgram(Gen<Module> source, RejectionSink sin
}
});

/// <summary>
/// The admission rule of <see cref="WellTypedProgram"/> for ONE sample, without the forced GC — for a
/// lane that collects its own programs in a loop (the differential sweep's generated arm, which runs
/// in a parallel class): true when the unparsed <paramref name="module"/> <c>Analyze</c>s clean,
/// otherwise the sample is recorded on <paramref name="sink"/> exactly as the filter records it.
/// </summary>
public static bool AdmitWellTyped(Module module, RejectionSink sink) =>
Admit(sink, () => Sharpy.Compiler.Pretty.Unparser.Unparse(module), module, AnalyseSingleFile);

/// <summary>Samples whose unparsed source <c>Compile</c>s clean (front end and C# emit).</summary>
public static Gen<Module> CompilableProgram(Gen<Module> source, RejectionSink sink) =>
source.Where(module =>
Expand Down
36 changes: 15 additions & 21 deletions src/Sharpy.Compiler.Tests/Properties/Semantic/AsyncPropertyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,31 +82,25 @@ public void AsyncFunction_Deterministic()
{
var source = Sharpy.Compiler.Pretty.Unparser.Unparse(module);

try
{
var compiler1 = new Sharpy.Compiler.Compiler();
var result1 = compiler1.Analyze(source, "async_test.spy");

var compiler2 = new Sharpy.Compiler.Compiler();
var result2 = compiler2.Analyze(source, "async_test.spy");
// A throw fails the sample: it used to be swallowed (#2288).
var compiler1 = new Sharpy.Compiler.Compiler();
var result1 = compiler1.Analyze(source, "async_test.spy");

if (result1.Success != result2.Success)
{
lock (errors)
errors.Add($"Success differs: {result1.Success} vs {result2.Success}");
}
var compiler2 = new Sharpy.Compiler.Compiler();
var result2 = compiler2.Analyze(source, "async_test.spy");

var diags1 = result1.Diagnostics.GetAll().Select(d => d.Code).OrderBy(c => c).ToList();
var diags2 = result2.Diagnostics.GetAll().Select(d => d.Code).OrderBy(c => c).ToList();
if (!diags1.SequenceEqual(diags2))
{
lock (errors)
errors.Add($"Diagnostics differ: [{string.Join(",", diags1)}] vs [{string.Join(",", diags2)}]");
}
if (result1.Success != result2.Success)
{
lock (errors)
errors.Add($"Success differs: {result1.Success} vs {result2.Success}");
}
catch

var diags1 = result1.Diagnostics.GetAll().Select(d => d.Code).OrderBy(c => c).ToList();
var diags2 = result2.Diagnostics.GetAll().Select(d => d.Code).OrderBy(c => c).ToList();
if (!diags1.SequenceEqual(diags2))
{
// Swallow
lock (errors)
errors.Add($"Diagnostics differ: [{string.Join(",", diags1)}] vs [{string.Join(",", diags2)}]");
}
}, print: m => Sharpy.Compiler.Pretty.Unparser.Unparse(m), iter: 50);

Expand Down
Loading
Loading