Files
60eac7f1d4 feat(compiler): add Compiler Result wrapper and diagnostic messages (#4) (#48)
Implement the uniform result wrapper every stage returns (MASTER_SPEC §8.6, §21),
in compiler/common/result.py.

- Severity: StrEnum with the four levels (Information/Warning/Error/Fatal, §21.1).
- Message: immutable, machine-readable diagnostic (code, severity, title,
  description, optional read-only context) with lossless to_json/from_json.
- CompilerResult: immutable wrapper (data, warnings, errors, metadata) with
  success/has_warnings properties and functional add_warning/add_error/merge
  returning new instances. merge concatenates warnings then errors
  deterministically, takes the downstream data, and unions metadata.
- to_json serializes stage data via its own to_json when available.

Tests: construction, message round-trip, severity handling, immutability,
deterministic merge, and machine-readable serialization.

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:07:25 +02:00

161 lines
5.6 KiB
Python

"""The Compiler Result wrapper and diagnostic messages (MASTER_SPEC §8.6, §21).
Every compiler stage returns a :class:`CompilerResult` exposing the same four
fields — ``data``, ``warnings``, ``errors``, ``metadata`` — so results can be
merged uniformly as data flows down the pipeline.
Diagnostics are :class:`Message` objects with a stable machine-readable ``code``
and ``severity``; no field requires string parsing to interpret. Results are
immutable: :meth:`CompilerResult.add_warning`, :meth:`CompilerResult.add_error`,
and :meth:`CompilerResult.merge` all return new instances.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from enum import StrEnum
from types import MappingProxyType
from typing import Any
_EMPTY_CONTEXT: Mapping[str, Any] = MappingProxyType({})
class Severity(StrEnum):
"""The four diagnostic severity levels (§21.1).
- INFORMATION — compiler activity; never requires user action.
- WARNING — a recoverable problem; compilation continues.
- ERROR — invalid intermediate data; the affected compilation stops.
- FATAL — the compiler cannot start or continue at all.
"""
INFORMATION = "information"
WARNING = "warning"
ERROR = "error"
FATAL = "fatal"
@dataclass(frozen=True)
class Message:
"""A single diagnostic message (§21.3).
``context`` is an optional machine-readable mapping (e.g. the offending
field or stage); it is stored read-only so a produced message is immutable.
"""
code: str
severity: Severity
title: str
description: str
context: Mapping[str, Any] = field(default_factory=lambda: _EMPTY_CONTEXT)
def __post_init__(self) -> None:
# Normalize context to a read-only view without breaking frozen semantics.
if not isinstance(self.context, MappingProxyType):
frozen = MappingProxyType(dict(self.context)) if self.context else _EMPTY_CONTEXT
object.__setattr__(self, "context", frozen)
@classmethod
def from_json(cls, data: Mapping[str, Any]) -> Message:
return cls(
code=data["code"],
severity=Severity(data["severity"]),
title=data["title"],
description=data["description"],
context=data.get("context") or _EMPTY_CONTEXT,
)
def to_json(self) -> dict[str, Any]:
result: dict[str, Any] = {
"code": self.code,
"severity": self.severity.value,
"title": self.title,
"description": self.description,
}
if self.context:
result["context"] = dict(self.context)
return result
@dataclass(frozen=True)
class CompilerResult:
"""The uniform wrapper returned by every compiler stage (§8.6).
Attributes:
data: The stage's payload (a model, a dict, or None). Not interpreted here.
warnings: Recoverable diagnostics; compilation continues.
errors: Error/Fatal diagnostics; the affected compilation has stopped.
metadata: Free-form machine-readable stage metadata (read-only).
"""
data: Any = None
warnings: tuple[Message, ...] = ()
errors: tuple[Message, ...] = ()
metadata: Mapping[str, Any] = field(default_factory=lambda: _EMPTY_CONTEXT)
def __post_init__(self) -> None:
if not isinstance(self.metadata, MappingProxyType):
frozen = MappingProxyType(dict(self.metadata)) if self.metadata else _EMPTY_CONTEXT
object.__setattr__(self, "metadata", frozen)
@property
def success(self) -> bool:
"""True when no Error or Fatal diagnostics have been recorded."""
return not self.errors
@property
def has_warnings(self) -> bool:
return bool(self.warnings)
def add_warning(self, message: Message) -> CompilerResult:
"""Return a new result with ``message`` appended to the warnings."""
return CompilerResult(
data=self.data,
warnings=(*self.warnings, message),
errors=self.errors,
metadata=self.metadata,
)
def add_error(self, message: Message) -> CompilerResult:
"""Return a new result with ``message`` appended to the errors."""
return CompilerResult(
data=self.data,
warnings=self.warnings,
errors=(*self.errors, message),
metadata=self.metadata,
)
def merge(self, other: CompilerResult) -> CompilerResult:
"""Combine two results as data flows from this stage to the next.
Warnings and errors are concatenated deterministically (this result's
first, then ``other``'s). The merged result carries ``other``'s data
(the downstream stage's output) and the union of both metadata mappings
(``other`` wins on key conflicts).
"""
return CompilerResult(
data=other.data,
warnings=(*self.warnings, *other.warnings),
errors=(*self.errors, *other.errors),
metadata={**self.metadata, **other.metadata},
)
def to_json(self) -> dict[str, Any]:
"""Serialize to a machine-readable dict (data serialized when possible)."""
return {
"success": self.success,
"data": _serialize_data(self.data),
"warnings": [w.to_json() for w in self.warnings],
"errors": [e.to_json() for e in self.errors],
"metadata": dict(self.metadata),
}
def _serialize_data(data: Any) -> Any:
"""Serialize stage data: use ``to_json`` when available, else pass through."""
to_json = getattr(data, "to_json", None)
if callable(to_json):
return to_json()
return data