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
4 changes: 4 additions & 0 deletions transpiler/codegen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .contract import ContractGenerator
from .generator import TypeScriptCodeGenerator
from .metadata import MetadataExtractor, FactoryGenerator, ContractMetadata
from .diagnostics import TranspilerDiagnostics, Diagnostic, DiagnosticSeverity

__all__ = [
'YulTranspiler',
Expand All @@ -34,4 +35,7 @@
'MetadataExtractor',
'FactoryGenerator',
'ContractMetadata',
'TranspilerDiagnostics',
'Diagnostic',
'DiagnosticSeverity',
]
11 changes: 11 additions & 0 deletions transpiler/codegen/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ..parser.ast_nodes import TypeName
from ..type_system import TypeRegistry
from .diagnostics import TranspilerDiagnostics


# Reserved JavaScript method names that conflict with Object.prototype or other built-ins
Expand Down Expand Up @@ -95,6 +96,16 @@ class CodeGenerationContext:
# Reference to the full registry (for complex queries)
_registry: Optional[TypeRegistry] = None

# Diagnostics collector
_diagnostics: Optional[TranspilerDiagnostics] = None

@property
def diagnostics(self) -> TranspilerDiagnostics:
"""Get the diagnostics collector, creating one if needed."""
if self._diagnostics is None:
self._diagnostics = TranspilerDiagnostics()
return self._diagnostics

def indent(self) -> str:
"""Return the current indentation string."""
return self.indent_str * self.indent_level
Expand Down
242 changes: 242 additions & 0 deletions transpiler/codegen/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
"""
Diagnostic/warning system for the transpiler.

Collects and reports warnings about unsupported Solidity constructs
that were skipped or degraded during transpilation. Helps developers
understand simulation fidelity gaps.
"""

import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional


class DiagnosticSeverity(Enum):
"""Severity levels for transpiler diagnostics."""
WARNING = 'warning'
INFO = 'info'


@dataclass
class Diagnostic:
"""A single diagnostic message."""
severity: DiagnosticSeverity
code: str
message: str
file_path: str = ''
line: Optional[int] = None
construct: str = '' # e.g., 'modifier', 'try/catch', 'receive'

def __str__(self) -> str:
location = self.file_path
if self.line:
location = f'{location}:{self.line}'
if location:
return f'[{self.severity.value}] {location}: {self.message} ({self.code})'
return f'[{self.severity.value}] {self.message} ({self.code})'


class TranspilerDiagnostics:
"""
Collects transpiler warnings/diagnostics during code generation.

Usage:
diag = TranspilerDiagnostics()
diag.warn_modifier_stripped("onlyOwner", "Engine.sol", line=42)
# ... after transpilation ...
diag.print_summary()
"""

def __init__(self, verbose: bool = False):
self._diagnostics: List[Diagnostic] = []
self._verbose = verbose

@property
def diagnostics(self) -> List[Diagnostic]:
"""Get all collected diagnostics."""
return list(self._diagnostics)

@property
def warnings(self) -> List[Diagnostic]:
"""Get only warning-level diagnostics."""
return [d for d in self._diagnostics if d.severity == DiagnosticSeverity.WARNING]

@property
def count(self) -> int:
"""Get total diagnostic count."""
return len(self._diagnostics)

def clear(self) -> None:
"""Clear all diagnostics."""
self._diagnostics.clear()

# =========================================================================
# SPECIFIC WARNING METHODS
# =========================================================================

def warn_modifier_stripped(
self,
modifier_name: str,
file_path: str = '',
line: Optional[int] = None,
) -> None:
"""Warn that a modifier was stripped (not inlined)."""
self._diagnostics.append(Diagnostic(
severity=DiagnosticSeverity.WARNING,
code='W001',
message=f'Modifier "{modifier_name}" was stripped (not inlined). '
f'Access control and validation logic may be missing.',
file_path=file_path,
line=line,
construct='modifier',
))

def warn_try_catch_skipped(
self,
file_path: str = '',
line: Optional[int] = None,
) -> None:
"""Warn that a try/catch block was skipped."""
self._diagnostics.append(Diagnostic(
severity=DiagnosticSeverity.WARNING,
code='W002',
message='try/catch block was skipped (empty block generated). '
'Error handling logic is missing.',
file_path=file_path,
line=line,
construct='try/catch',
))

def warn_receive_fallback_skipped(
self,
kind: str,
file_path: str = '',
line: Optional[int] = None,
) -> None:
"""Warn that receive() or fallback() was skipped."""
self._diagnostics.append(Diagnostic(
severity=DiagnosticSeverity.WARNING,
code='W003',
message=f'{kind}() function was skipped (not supported).',
file_path=file_path,
line=line,
construct=kind,
))

def warn_function_pointer_unsupported(
self,
file_path: str = '',
line: Optional[int] = None,
) -> None:
"""Warn that a function pointer type was encountered."""
self._diagnostics.append(Diagnostic(
severity=DiagnosticSeverity.WARNING,
code='W004',
message='Function pointer type is not supported; using generic type.',
file_path=file_path,
line=line,
construct='function pointer',
))

def warn_yul_parse_error(
self,
error: str,
file_path: str = '',
line: Optional[int] = None,
) -> None:
"""Warn that Yul code could not be parsed."""
self._diagnostics.append(Diagnostic(
severity=DiagnosticSeverity.WARNING,
code='W005',
message=f'Yul parse error: {error}. Assembly block may be incorrect.',
file_path=file_path,
line=line,
construct='assembly',
))

def warn_unsupported_construct(
self,
construct: str,
detail: str = '',
file_path: str = '',
line: Optional[int] = None,
) -> None:
"""Generic warning for unsupported constructs."""
msg = f'Unsupported construct: {construct}'
if detail:
msg += f' ({detail})'
self._diagnostics.append(Diagnostic(
severity=DiagnosticSeverity.WARNING,
code='W099',
message=msg,
file_path=file_path,
line=line,
construct=construct,
))

def info_runtime_replacement(
self,
file_path: str,
replacement_path: str,
) -> None:
"""Info that a file uses a runtime replacement."""
self._diagnostics.append(Diagnostic(
severity=DiagnosticSeverity.INFO,
code='I001',
message=f'Using runtime replacement: {replacement_path}',
file_path=file_path,
construct='runtime-replacement',
))

# =========================================================================
# REPORTING
# =========================================================================

def print_summary(self, file=None) -> None:
"""Print a summary of all diagnostics to stderr (or specified file)."""
if file is None:
file = sys.stderr

if not self._diagnostics:
return

warnings = self.warnings
infos = [d for d in self._diagnostics if d.severity == DiagnosticSeverity.INFO]

if warnings:
print(f'\nTranspiler warnings ({len(warnings)}):', file=file)
# Group by construct type
by_construct: dict = {}
for w in warnings:
key = w.construct or 'other'
if key not in by_construct:
by_construct[key] = []
by_construct[key].append(w)

for construct, diags in sorted(by_construct.items()):
print(f' {construct}: {len(diags)} occurrence(s)', file=file)
if self._verbose:
for d in diags:
print(f' {d}', file=file)

if infos and self._verbose:
print(f'\nTranspiler info ({len(infos)}):', file=file)
for d in infos:
print(f' {d}', file=file)

def get_summary(self) -> str:
"""Get a summary string of all diagnostics."""
if not self._diagnostics:
return 'No transpiler warnings.'

warnings = self.warnings
by_construct: dict = {}
for w in warnings:
key = w.construct or 'other'
if key not in by_construct:
by_construct[key] = 0
by_construct[key] += 1

parts = [f'{count} {construct}' for construct, count in sorted(by_construct.items())]
return f'Transpiler warnings: {", ".join(parts)}'
73 changes: 36 additions & 37 deletions transpiler/codegen/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ def __init__(
super().__init__(ctx)
self._type_converter = type_converter
self._registry = registry
self._abi_inferer: Optional['AbiTypeInferer'] = None

def _get_abi_inferer(self) -> 'AbiTypeInferer':
"""Get or create an AbiTypeInferer with current context state."""
from .abi import AbiTypeInferer
# Rebuild on every call since context (var_types, method_return_types) changes per function
self._abi_inferer = AbiTypeInferer(
var_types=self._ctx.var_types,
known_enums=self._ctx.known_enums,
known_contracts=self._ctx.known_contracts,
known_interfaces=self._ctx.known_interfaces,
known_struct_fields=self._ctx.known_struct_fields,
method_return_types=self._ctx.current_method_return_types,
)
return self._abi_inferer

# =========================================================================
# MAIN DISPATCH
Expand Down Expand Up @@ -547,16 +562,25 @@ def generate_index_access(self, access: IndexAccess) -> str:
key_type_name = type_info.key_type.name if type_info.key_type.name else ''
mapping_has_numeric_key = key_type_name.startswith('uint') or key_type_name.startswith('int')

# Check for struct field access with known mapping fields
# Check for struct field access using type registry
if isinstance(access.base, MemberAccess):
member_name = access.base.member
numeric_key_mapping_fields = {
'p0Team', 'p1Team', 'p0States', 'p1States',
'globalEffects', 'p0Effects', 'p1Effects', 'engineHooks'
}
if member_name in numeric_key_mapping_fields:
is_mapping = True
mapping_has_numeric_key = True
# Try to resolve the struct type of the parent object
parent_var = self._get_base_var_name(access.base.expression) if hasattr(access.base, 'expression') else None
if parent_var and parent_var in self._ctx.var_types:
parent_type = self._ctx.var_types[parent_var]
struct_name = parent_type.name if parent_type else ''
if struct_name and struct_name in self._ctx.known_struct_fields:
field_info = self._ctx.known_struct_fields[struct_name].get(member_name)
if field_info:
field_type = field_info[0] if isinstance(field_info, tuple) else field_info
field_is_array = field_info[1] if isinstance(field_info, tuple) else False
# Arrays and mappings with numeric keys need Number() conversion
if field_is_array:
is_likely_array = True
elif field_type and (field_type.startswith('mapping')):
is_mapping = True
mapping_has_numeric_key = True

# Determine if we need Number conversion
needs_number_conversion = is_likely_array or (is_mapping and mapping_has_numeric_key)
Expand Down Expand Up @@ -620,45 +644,20 @@ def generate_type_cast(self, cast: TypeCast) -> str:
return self._type_converter.generate_type_cast(cast, self.generate)

# =========================================================================
# ABI ENCODING HELPERS
# ABI ENCODING HELPERS (delegated to AbiTypeInferer)
# =========================================================================

def _convert_abi_types(self, types_expr: Expression) -> str:
"""Convert Solidity type tuple to viem ABI parameter format."""
if isinstance(types_expr, TupleExpression):
type_strs = []
for comp in types_expr.components:
if comp:
type_strs.append(self._solidity_type_to_abi_param(comp))
return f'[{", ".join(type_strs)}]'
return f'[{self._solidity_type_to_abi_param(types_expr)}]'

def _solidity_type_to_abi_param(self, type_expr: Expression) -> str:
"""Convert a Solidity type expression to viem ABI parameter object."""
if isinstance(type_expr, Identifier):
name = type_expr.name
if name.startswith('uint') or name.startswith('int') or name == 'address' or name == 'bool' or name.startswith('bytes'):
return f"{{type: '{name}'}}"
if name in self._ctx.known_enums:
return "{type: 'uint8'}"
return "{type: 'bytes'}"
return "{type: 'bytes'}"
return self._get_abi_inferer().convert_types_expr(types_expr)

def _infer_abi_types_from_values(self, args: List[Expression]) -> str:
"""Infer ABI types from value expressions (for abi.encode)."""
type_strs = []
for arg in args:
type_str = self._infer_single_abi_type(arg)
type_strs.append(type_str)
return f'[{", ".join(type_strs)}]'
return self._get_abi_inferer().infer_abi_types(args)

def _infer_packed_abi_types(self, args: List[Expression]) -> str:
"""Infer packed ABI types from value expressions (for abi.encodePacked)."""
type_strs = []
for arg in args:
type_str = self._infer_single_packed_type(arg)
type_strs.append(f"'{type_str}'")
return f'[{", ".join(type_strs)}]'
return self._get_abi_inferer().infer_packed_types(args)

def _infer_expression_type(self, arg: Expression) -> tuple:
"""Infer the Solidity type from an expression.
Expand Down
Loading