Skip to content
Merged

Dev #2373

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
9 changes: 0 additions & 9 deletions build_tools/spec_blocks_allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ events.md::511f0d2735 # SPY0200 first #1952
events.md::5d33d34876 # SPY0420 first #1952
events.md::6c35a3c709 # SPY0200 first #1952
events.md::77bbdcdca8 # SPY0200 first #1952
events.md::7e16c0b9bc # SPY0202 first #1952
events.md::a692edaeaf # SPY0202 first #1952
events.md::b337a4b650 # SPY0200 first #1952
events.md::b83e0c5310 # SPY0420 first #1952
Expand Down Expand Up @@ -346,14 +345,6 @@ primitive_types.md::412c2923ce # SPY0346 first #1952
program_entry_point.md::08fda2fe5d # SPY0200 first #1952
program_entry_point.md::3e9510358b # SPY0200 first #1952
program_entry_point.md::daa5d09e86 # SPY0200 first #1952
properties.md::2cad2b350f # SPY0203 first #1952
properties_function_style.md::4538adfdac # SPY0407 first #1952
properties_function_style.md::86bb0df588 # SPY0496 first #1952
properties_function_style.md::b43ba33646 # SPY0406 first #1952
properties_function_style.md::c8231a5042 # SPY0203 first #1952
properties_function_style.md::d61225a7b3 # SPY0232 first #1952
properties_function_style.md::ec45a38dac # SPY0290 first #1952
properties_function_style.md::f31d92c964 # SPY0202 first #1952
properties_inheritance.md::7553d3f5f3 # SPY0203 first #1952
properties_inheritance.md::7695a1ea67 # SPY0493 first #1952
properties_inheritance.md::b20a3d0647 # SPY0202 first #1952
Expand Down
4 changes: 4 additions & 0 deletions docs/language_specification/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ class CustomControl(BaseControl):
### Abstract Events

```python
class UpdateEventArgs(EventArgs):
pass

@abstract
class BasePublisher:
@abstract
event on_update: EventHandler[UpdateEventArgs]
Expand Down
6 changes: 6 additions & 0 deletions docs/language_specification/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ For complete details on function-style properties, static properties, mixed acce
Properties participate in inheritance using `@virtual`, `@abstract`, `@override`, and `@final` decorators:

```python
@abstract
class Shape:
@abstract
property get area(self) -> float:
Expand All @@ -155,6 +156,11 @@ class Shape:
return "Shape"

class Circle(Shape):
radius: float

def __init__(self, radius: float):
self.radius = radius

@override
property get area(self) -> float:
return 3.14159 * self.radius ** 2
Expand Down
68 changes: 50 additions & 18 deletions docs/language_specification/properties_function_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ For properties requiring custom logic (validation, transformation, computation),
You can combine an auto-property declaration with a custom getter, setter, or both. The auto-property declaration defines the type and a backing field (`_name`), while the custom accessor provides the logic:

```python
# Auto-getter generated, custom setter validates
property name: str
property set name(self, value: str):
if len(value) > 0:
self._name = value
class User:
# Auto-getter generated, custom setter validates
property name: str = ""
property set name(self, value: str):
if len(value) > 0:
self._name = value
```

**Semantics:**
Expand Down Expand Up @@ -74,15 +75,13 @@ public float Diagonal {

```python
class Temperature:
_celsius: float = 0.0

# Function-style setter with validation
property set celsius(self, value: float):
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value

# The auto half supplies the storage — do NOT also declare `_celsius`
# The auto half supplies the storage (`_celsius`) — do NOT also declare it
property celsius: float = 0.0
```

Expand Down Expand Up @@ -117,8 +116,25 @@ class Box:
self._value = v
```

Two shapes leave that constraint, and both are refused with **SPY0496**:
A setter also returns nothing to C#, so its return annotation, when written, is `None` — the
accessor's own return, never the property's type. The property's type is always the value
parameter's, so this set-only property is an `int` property:

<!-- spec-sweep: output "5" -->
```python
class Gauge:
# `-> None` is the setter's own return; the property's type is `v`'s: int
property set level(self, v: int) -> None:
print(v)

def main() -> None:
g = Gauge()
g.level = 5
```

Three shapes leave that constraint, and each is refused with **SPY0496**:

<!-- spec-sweep: error SPY0496 -->
```python
class Broken:
# ❌ A getter receives no value, so `factor` has nothing to denote —
Expand All @@ -129,6 +145,10 @@ class Broken:
# ❌ An accessor has no argument list to vary
property set samples(self, *values: int):
print(len(values))

# ❌ A setter returns nothing to C# — its return annotation is `None` or nothing
property set level(self, v: int) -> int:
print(v)
```

The rule applies wherever a property is declared: in a class, a struct, an interface, a nested type,
Expand All @@ -140,6 +160,7 @@ See [Function Variadic Arguments](function_variadic_arguments.md#not-in-a-proper

Getters and setters can have different visibility:

<!-- spec-sweep: output -->
```python
class Counter:
_value: int = 0
Expand All @@ -160,11 +181,18 @@ class Counter:
def reset(self):
self.value = 0

# Usage
c = Counter()
print(c.value) # OK: public getter
c.increment() # OK: internal modification via public method
c.value = 10 # ERROR: setter is private
def main() -> None:
c = Counter()
print(c.value) # 0
c.increment() # the public method modifies it through the private setter
print(c.value) # 1
```

Outside the class the setter is not accessible:

<!-- spec-sweep: fragment -->
```python
c.value = 10 # ❌ ERROR: the setter of 'value' is private to Counter
```

**With Auto-Properties:**
Expand Down Expand Up @@ -201,8 +229,10 @@ public int Value {

Use `@static` decorator for class-level properties. Static properties take no `self` parameter:

<!-- spec-sweep: output -->
```python
class AppConfig:
@static
_debug_mode: bool = False
_instance_count: int = 0

Expand All @@ -227,10 +257,10 @@ class AppConfig:
property get debug_mode() -> bool:
return AppConfig._debug_mode

# Usage
print(AppConfig.version) # "1.0.0"
AppConfig.debug_mode = True
print(AppConfig.is_debug_enabled) # True
def main() -> None:
print(AppConfig.version) # 1.0.0
AppConfig.debug_mode = True
print(AppConfig.is_debug_enabled) # True
```

**Static Property Rules:**
Expand Down Expand Up @@ -275,6 +305,7 @@ property set debug_mode(value: bool):
property default_timeout: float = 30.0
```

<!-- spec-sweep: fragment -->
```python
# Usage from another module
import os
Expand Down Expand Up @@ -319,6 +350,7 @@ public static partial class OsModule

A property and a method cannot share the same name within a class:

<!-- spec-sweep: error SPY0406 -->
```python
class Example:
_value: int = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,44 @@ internal static partial class ErrorInjector
// ---- annotations ------------------------------------------------------------------------------

public static InjectionResult? InjectUndefinedTypeAtParameter(Module module) =>
Lane(MutationLanes.UndefinedTypeAtParameter(module), "SPY0202");
WithAbsOfUnknownCascadePin(Lane(MutationLanes.UndefinedTypeAtParameter(module), "SPY0202"));

/// <summary>
/// #2075 (R-JE): an undefined parameter type leaves the parameter Unknown, and <c>abs(p)</c> on it
/// is typed by abs's FIRST overload, int8 — never Unknown. Two measured faces of that one cell
/// (built sharpyc @ a78e3aa71): in a condition, SPY0220 "If condition must be boolean, got 'int8' —
/// has no falsy case"; stored back into the parameter and later met by an <c>int</c> in a
/// conditional expression, SPY0227 "no best common type". Over 17 builtins called on an
/// undefined-typed argument in an <c>if</c> and a <c>while</c> test only <c>abs</c> cascades with
/// SPY0220 (<c>sum</c> draws SPY0354, a shape this lane never generates). Before the pin the lane
/// was red on ~1 seed in 80 (the SPY0220 face, the P21f gate @ 6a0b507f1) and the SPY0227 face
/// surfaced at 2000 samples.
/// </summary>
internal static readonly KnownRedCascade AbsOfUnknownCascade = new KnownRedCascade("#2075", new[] { "SPY0220", "SPY0227" },
"abs(<parameter of an undefined type>) is typed int8: refused as a condition, or with no best common type beside an int");

/// <summary>Attaches <see cref="AbsOfUnknownCascade"/> only when a function of the mutated module
/// passes a parameter annotated with the injected undefined type to <c>abs</c>.</summary>
private static InjectionResult? WithAbsOfUnknownCascadePin(InjectionResult? result)
{
if (result is null)
return null;

bool shape = Sharpy.Compiler.Tests.Infrastructure.AstDescendants.SelfAndDescendants(result.Mutated)
.OfType<FunctionDef>()
.Any(function =>
{
var unknown = function.Parameters
.Where(p => p.Type?.Name == PositionMutations.UndefinedType)
.Select(p => p.Name)
.ToHashSet(StringComparer.Ordinal);
return unknown.Count > 0
&& Sharpy.Compiler.Tests.Infrastructure.AstDescendants.SelfAndDescendants(function).Any(n =>
n is FunctionCall { Function: Identifier { Name: "abs" } } call
&& call.Arguments.Any(a => a is Identifier id && unknown.Contains(id.Name)));
});
return shape ? result with { KnownCascade = AbsOfUnknownCascade } : result;
}

public static InjectionResult? InjectUndefinedTypeAtBase(Module module) =>
Lane(MutationLanes.UndefinedTypeAtBase(module), "SPY0202");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,32 @@ internal static partial class ErrorInjector
/// to still analyse clean, so the first refusal fails the row and the pin drains with its issue.
/// <see cref="ExpectedDiagnosticCode"/> is null only on a pin whose refusal has no code yet.
/// <see cref="Severity"/> is the severity the code is drawn at — <c>Error</c> unless the one
/// broken fact is a warning (SPY0463); see <see cref="JudgeSource"/>.
/// broken fact is a warning (SPY0463); see <see cref="JudgeSource"/>. <see cref="KnownCascade"/>
/// pins a follow-on code the compiler draws today beside the expected one, on one measured shape.
/// </summary>
public record InjectionResult(
Module Mutated,
string? ExpectedDiagnosticCode,
KnownRedRefusal? KnownRed = null,
CompilerDiagnosticSeverity Severity = CompilerDiagnosticSeverity.Error);
CompilerDiagnosticSeverity Severity = CompilerDiagnosticSeverity.Error,
KnownRedCascade? KnownCascade = null);

/// <summary>A refusal the compiler does not make today: <paramref name="Issue"/> tracks it, and
/// <paramref name="Observed"/> says what happens instead.</summary>
public sealed record KnownRedRefusal(string Issue, string Observed);

/// <summary>
/// A cascade the compiler draws today (#2075, R-JE): beside the injection's one code, follow-on
/// codes from <paramref name="Tolerated"/> — the MEASURED faces of one known cell of
/// <paramref name="Issue"/> ("one mistake, one diagnostic" broken by the compiler, not by the
/// injection breaking a second fact). Attached by a lane only on the measured shape that draws it,
/// so every other sample of the lane is still judged exactly; under it the verdict accepts the
/// expected code alone, or the expected code plus tolerated codes, and refuses any other code. It
/// drains with the error sentinel (P17 phase 1, R-AZ); the deterministic facts on each face's
/// program fail the first time it stops cascading.
/// </summary>
public sealed record KnownRedCascade(string Issue, string[] Tolerated, string Observed);

/// <summary>
/// Unparses the injected program and judges it through <see cref="JudgeSource"/> at the
/// injection's <see cref="InjectionResult.Severity"/> — this overload adds nothing but the
Expand All @@ -36,7 +50,8 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
public static string? Judge(InjectionResult injection, string label)
{
var source = Sharpy.Compiler.Pretty.Unparser.Unparse(injection.Mutated);
return JudgeSource(source, injection.ExpectedDiagnosticCode, injection.Severity, injection.KnownRed, label);
return JudgeSource(source, injection.ExpectedDiagnosticCode, injection.Severity, injection.KnownRed, label,
injection.KnownCascade);
}

/// <summary>
Expand All @@ -61,8 +76,9 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
string? expectedCode,
CompilerDiagnosticSeverity severity,
KnownRedRefusal? pin,
string label) =>
JudgeDiagnostics(() => AnalyseSingleFile(source), source, expectedCode, severity, pin, label);
string label,
KnownRedCascade? cascade = null) =>
JudgeDiagnostics(() => AnalyseSingleFile(source), source, expectedCode, severity, pin, label, cascade);

/// <summary>
/// The multi-file front-end of the same verdict: compiles <paramref name="files"/> (relative
Expand Down Expand Up @@ -92,11 +108,16 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
string? expectedCode,
CompilerDiagnosticSeverity severity,
KnownRedRefusal? pin,
string label)
string label,
KnownRedCascade? cascade = null)
{
if (severity is not (CompilerDiagnosticSeverity.Error or CompilerDiagnosticSeverity.Warning))
throw new ArgumentOutOfRangeException(nameof(severity), severity,
"A refusal lane is judged at Error or Warning severity");
if (cascade is not null && (pin is not null || severity != CompilerDiagnosticSeverity.Error))
throw new ArgumentException(
"A cascade pin rides an error-arm injection that has an expected code, never a KnownRed refusal pin",
nameof(cascade));

var source = sourceForMessage;
List<string> errors;
Expand Down Expand Up @@ -145,9 +166,18 @@ public sealed record KnownRedRefusal(string Issue, string Observed);
: $"Injected {label} must draw exactly one warning, {expected}, and no error, drew {drewAll}:\n{source}";
}

return errors is [var only] && only == expected
? null
: $"Injected {label} must draw exactly {expected}, drew {drewAll}:\n{source}";
if (errors is [var only] && only == expected)
return null;

// The pinned cascade (#2075, R-JE): the expected code plus follow-ons from the tolerated faces only.
if (cascade is not null && errors.Contains(expected)
&& errors.All(c => c == expected || cascade.Tolerated.Contains(c)))
return null;

var allowed = cascade is null
? expected
: $"{expected} (or {expected} + [{string.Join(", ", cascade.Tolerated)}], pinned on {cascade.Issue})";
return $"Injected {label} must draw exactly {allowed}, drew {drewAll}:\n{source}";
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,8 @@ private void RunInjectionTest(
var failures = new List<string>();
var sources = new HashSet<string>(StringComparer.Ordinal);
ErrorInjector.KnownRedRefusal? pin = null;
ErrorInjector.KnownRedCascade? cascade = null;
int onCascadeShape = 0;

var baseGen = overrideGen ?? Gen.OneOfConst("int", "str", "bool").SelectMany(type =>
GenTyped.TypedProgram(TypeEnv.Default, type, fuel: 2));
Expand All @@ -649,6 +651,11 @@ private void RunInjectionTest(

Interlocked.Increment(ref tested);
pin = result.KnownRed;
if (result.KnownCascade is { } known)
{
cascade = known;
Interlocked.Increment(ref onCascadeShape);
}
var mutatedSource = Sharpy.Compiler.Pretty.Unparser.Unparse(result.Mutated);
lock (sources)
sources.Add(mutatedSource);
Expand All @@ -669,6 +676,9 @@ private void RunInjectionTest(
_output.WriteLine(pin is null
? $"Error completeness ({label}): {tested - failures.Count}/{tested} drew the expected code ({sources.Count} distinct mutated sources)"
: $"Error completeness ({label}): KnownRed {pin.Issue} — {tested - failures.Count}/{tested} still unrefused ({pin.Observed}; {sources.Count} distinct mutated sources)");
if (cascade is not null)
_output.WriteLine($"Error completeness ({label}): KnownRedCascade {cascade.Issue} — {onCascadeShape}/{tested} on the pinned shape, "
+ $"judged with [{string.Join(", ", cascade.Tolerated)}] tolerated ({cascade.Observed})");
Assert.True(tested > 0, $"No {label} injections were applicable");
Assert.True(failures.Count == 0,
$"{failures.Count}/{tested} {label} injections misjudged; the first:\n{failures.FirstOrDefault()}");
Expand Down
Loading
Loading