This commit is contained in:
lz_db
2025-11-16 12:31:03 +08:00
commit 0fab423a18
1451 changed files with 743213 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
from __future__ import annotations
from abc import ABC
from collections import OrderedDict
from dataclasses import dataclass
from typing import List
class CairoType(ABC):
"""
Base type for all Cairo type representations. All types extend it.
"""
@dataclass
class FeltType(CairoType):
"""
Type representation of Cairo field element.
"""
@dataclass
class BoolType(CairoType):
"""
Type representation of Cairo boolean.
"""
@dataclass
class TupleType(CairoType):
"""
Type representation of Cairo tuples without named fields.
"""
types: List[CairoType] #: types of every tuple element.
@dataclass
class NamedTupleType(CairoType):
"""
Type representation of Cairo tuples with named fields.
"""
types: OrderedDict[str, CairoType] #: types of every tuple member.
@dataclass
class ArrayType(CairoType):
"""
Type representation of Cairo arrays.
"""
inner_type: CairoType #: type of element inside array.
@dataclass
class StructType(CairoType):
"""
Type representation of Cairo structures.
"""
name: str #: Structure name
# We need ordered dict, because it is important in serialization
types: OrderedDict[str, CairoType] #: types of every structure member.
@dataclass
class EnumType(CairoType):
"""
Type representation of Cairo enums.
"""
name: str
variants: OrderedDict[str, CairoType]
@dataclass
class OptionType(CairoType):
"""
Type representation of Cairo options.
"""
type: CairoType
@dataclass
class UintType(CairoType):
"""
Type representation of Cairo unsigned integers.
"""
bits: int
def check_range(self, value: int):
"""
Utility method checking if the `value` is in range.
"""
@dataclass
class TypeIdentifier(CairoType):
"""
Type representation of Cairo identifiers.
"""
name: str
@dataclass
class UnitType(CairoType):
"""
Type representation of Cairo unit `()`.
"""
@dataclass
class EventType(CairoType):
"""
Type representation of Cairo Event.
"""
name: str
types: OrderedDict[str, CairoType]

View File

@@ -0,0 +1,77 @@
import dataclasses
from typing import List, Optional
class CairoType:
"""
Base class for cairo types.
"""
@dataclasses.dataclass
class TypeFelt(CairoType):
pass
@dataclasses.dataclass
class TypeCodeoffset(CairoType):
pass
@dataclasses.dataclass
class TypePointer(CairoType):
pointee: CairoType
@dataclasses.dataclass
class TypeIdentifier(CairoType):
"""
Represents a name of an unresolved type.
This type can be resolved to TypeStruct or TypeDefinition.
"""
name: str
@dataclasses.dataclass
class TypeStruct(CairoType):
scope: str
@dataclasses.dataclass
class TypeFunction(CairoType):
"""
Represents a type of a function.
"""
scope: str
@dataclasses.dataclass
class TypeTuple(CairoType):
"""
Represents a type of a named or unnamed tuple.
For example, "(felt, felt*)" or "(a: felt, b: felt*)".
"""
@dataclasses.dataclass
class Item(CairoType):
"""
Represents a possibly named type item of a TypeTuple.
For example: "felt" or "a: felt".
"""
name: Optional[str]
typ: CairoType
members: List["TypeTuple.Item"]
has_trailing_comma: bool = dataclasses.field(hash=False, compare=False)
@property
def is_named(self) -> bool:
return all(member.name is not None for member in self.members)
@dataclasses.dataclass
class ExprIdentifier(CairoType):
name: str

View File

@@ -0,0 +1,46 @@
from ....lark import Lark
from .cairo_types import CairoType
from .parser_transformer import ParserTransformer
CAIRO_EBNF = """
%import common.WS_INLINE
%ignore WS_INLINE
IDENTIFIER: /[a-zA-Z_][a-zA-Z_0-9]*/
_DBL_STAR: "**"
COMMA: ","
?type: non_identifier_type
| identifier -> type_struct
comma_separated{item}: item? (COMMA item)* COMMA?
named_type: identifier (":" type)? | non_identifier_type
non_identifier_type: "felt" -> type_felt
| "codeoffset" -> type_codeoffset
| type "*" -> type_pointer
| type _DBL_STAR -> type_pointer2
| "(" comma_separated{named_type} ")" -> type_tuple
identifier: IDENTIFIER ("." IDENTIFIER)*
"""
def parse(code: str) -> CairoType:
"""
Parses the given string and returns a CairoType.
"""
grammar = CAIRO_EBNF
grammar_parser = Lark(
grammar=grammar,
start=["type"],
parser="lalr",
)
parsed = grammar_parser.parse(code)
transformed = ParserTransformer().transform(parsed)
return transformed

View File

@@ -0,0 +1,138 @@
import dataclasses
from typing import Optional, Tuple
from ....lark import Token, Transformer, v_args
from .cairo_types import (
CairoType,
ExprIdentifier,
TypeCodeoffset,
TypeFelt,
TypeIdentifier,
TypePointer,
TypeStruct,
TypeTuple,
)
@dataclasses.dataclass
class ParserContext:
"""
Represents information that affects the parsing process.
"""
# If True, treat type identifiers as resolved.
resolved_types: bool = False
class ParserError(Exception):
"""
Base exception for parsing process.
"""
@dataclasses.dataclass
class CommaSeparated:
"""
Represents a list of comma separated values, such as expressions or types.
"""
args: list
has_trailing_comma: bool
class ParserTransformer(Transformer):
"""
Transforms the lark tree into an AST based on the classes defined in cairo_types.py.
"""
# pylint: disable=unused-argument, no-self-use
def __init__(self):
super().__init__()
self.parser_context = ParserContext()
def __default__(self, data: str, children, meta):
raise TypeError(f"Unable to parse tree node of type {data}")
def comma_separated(self, value) -> CommaSeparated:
saw_comma = None
args: list = []
for v in value:
if isinstance(v, Token) and v.type == "COMMA":
if saw_comma is not False:
raise ParserError("Unexpected comma.")
saw_comma = True
else:
if saw_comma is False:
raise ParserError("Expected a comma before this expression.")
args.append(v)
# Reset state.
saw_comma = False
if saw_comma is None:
saw_comma = False
return CommaSeparated(args=args, has_trailing_comma=saw_comma)
# Types.
@v_args(meta=True)
def named_type(self, meta, value) -> TypeTuple.Item:
name: Optional[str]
if len(value) == 1:
# Unnamed type.
(typ,) = value
name = None
if isinstance(typ, ExprIdentifier):
typ = self.type_struct([typ])
elif len(value) == 2:
# Named type.
identifier, typ = value
assert isinstance(identifier, ExprIdentifier)
assert isinstance(typ, CairoType)
if "." in identifier.name:
raise ParserError("Unexpected . in name.")
name = identifier.name
else:
raise NotImplementedError(f"Unexpected number of values. {value}")
return TypeTuple.Item(name=name, typ=typ)
@v_args(meta=True)
def type_felt(self, meta, value):
return TypeFelt()
@v_args(meta=True)
def type_codeoffset(self, meta, value):
return TypeCodeoffset()
def type_struct(self, value):
assert len(value) == 1 and isinstance(value[0], ExprIdentifier)
if self.parser_context.resolved_types:
# If parser_context.resolved_types is True, assume that the type is a struct.
return TypeStruct(scope=value[0].name)
return TypeIdentifier(name=value[0].name)
@v_args(meta=True)
def type_pointer(self, meta, value):
return TypePointer(pointee=value[0])
@v_args(meta=True)
def type_pointer2(self, meta, value):
return TypePointer(pointee=TypePointer(pointee=value[0]))
@v_args(meta=True)
def type_tuple(self, meta, value: Tuple[CommaSeparated]):
(lst,) = value
return TypeTuple(members=lst.args, has_trailing_comma=lst.has_trailing_comma)
@v_args(meta=True)
def identifier(self, meta, value):
return ExprIdentifier(name=".".join(x.value for x in value))
@v_args(meta=True)
def identifier_def(self, meta, value):
return ExprIdentifier(name=value[0].value)

View File

@@ -0,0 +1,64 @@
from typing import List
from ..constants import FIELD_PRIME
CairoData = List[int]
MAX_UINT256 = (1 << 256) - 1
MIN_UINT256 = 0
def uint256_range_check(value: int):
if not MIN_UINT256 <= value <= MAX_UINT256:
raise ValueError(
f"Uint256 is expected to be in range [0;2**256), got: {value}."
)
MIN_FELT = -FIELD_PRIME // 2
MAX_FELT = FIELD_PRIME // 2
def is_in_felt_range(value: int) -> bool:
return 0 <= value < FIELD_PRIME
def cairo_vm_range_check(value: int):
if not is_in_felt_range(value):
raise ValueError(
f"Felt is expected to be in range [0; {FIELD_PRIME}), got: {value}."
)
def encode_shortstring(text: str) -> int:
"""
A function which encodes short string value (at most 31 characters) into cairo felt (MSB as first character)
:param text: A short string value in python
:return: Short string value encoded into felt
"""
if len(text) > 31:
raise ValueError(
f"Shortstring cannot be longer than 31 characters, got: {len(text)}."
)
try:
text_bytes = text.encode("ascii")
except UnicodeEncodeError as u_err:
raise ValueError(f"Expected an ascii string. Found: {repr(text)}.") from u_err
value = int.from_bytes(text_bytes, "big")
cairo_vm_range_check(value)
return value
def decode_shortstring(value: int) -> str:
"""
A function which decodes a felt value to short string (at most 31 characters)
:param value: A felt value
:return: Decoded string which is corresponds to that felt
"""
cairo_vm_range_check(value)
return "".join([chr(i) for i in value.to_bytes(31, byteorder="big")]).lstrip("\x00")

View File

@@ -0,0 +1,121 @@
from __future__ import annotations
from collections import OrderedDict
from typing import Dict, cast
from .deprecated_parse import cairo_types as cairo_lang_types
from .data_types import (
ArrayType,
CairoType,
FeltType,
NamedTupleType,
StructType,
TupleType,
)
from .deprecated_parse.parser import parse
class UnknownCairoTypeError(ValueError):
"""
Error thrown when TypeParser finds type that was not declared prior to parsing.
"""
type_name: str
def __init__(self, type_name: str):
super().__init__(f"Type '{type_name}' is not defined")
self.type_name = type_name
class TypeParser:
"""
Low level utility class for parsing Cairo types that can be used in external methods.
"""
defined_types: Dict[str, StructType]
def __init__(self, defined_types: Dict[str, StructType]):
"""
TypeParser constructor.
:param defined_types: dictionary containing all defined types. For now, they can only be structures.
"""
self.defined_types = defined_types
for name, struct in defined_types.items():
if name != struct.name:
raise ValueError(
f"Keys must match name of type, '{name}' != '{struct.name}'."
)
def parse_inline_type(self, type_string: str) -> CairoType:
"""
Inline type is one that can be used inline, for instance as return type. For instance
(a: Uint256, b: felt*, c: (felt, felt)). Structure can only be referenced in inline type, can't be defined
this way.
:param type_string: type to parse.
"""
parsed = parse(type_string)
return self._transform_cairo_lang_type(parsed)
def _transform_cairo_lang_type(
self, cairo_type: cairo_lang_types.CairoType
) -> CairoType:
"""
For now, we use parse function from cairo-lang package. It will be replaced in the future, but we need to hide
it from the users.
This function takes types returned by cairo-lang package and maps them to our type classes.
:param cairo_type: type returned from parse_type function.
:return: CairoType defined by our package.
"""
if isinstance(cairo_type, cairo_lang_types.TypeFelt):
return FeltType()
if isinstance(cairo_type, cairo_lang_types.TypePointer):
return ArrayType(self._transform_cairo_lang_type(cairo_type.pointee))
if isinstance(cairo_type, cairo_lang_types.TypeIdentifier):
return self._get_struct(str(cairo_type.name))
if isinstance(cairo_type, cairo_lang_types.TypeTuple):
# Cairo returns is_named when there are no members
if cairo_type.is_named and len(cairo_type.members) != 0:
assert all(member.name is not None for member in cairo_type.members)
return NamedTupleType(
OrderedDict(
(
cast(
str, member.name
), # without that pyright is complaining
self._transform_cairo_lang_type(member.typ),
)
for member in cairo_type.members
)
)
return TupleType(
[
self._transform_cairo_lang_type(member.typ)
for member in cairo_type.members
]
)
# Contracts don't support codeoffset as input/output type, user can only use it if it was defined in types
if isinstance(cairo_type, cairo_lang_types.TypeCodeoffset):
return self._get_struct("codeoffset")
# Other options are: TypeFunction, TypeStruct
# Neither of them are possible. In particular TypeStruct is not possible because we parse structs without
# info about other structs, so they will be just TypeIdentifier (structure that was not parsed).
# This is an error of our logic, so we throw a RuntimeError.
raise RuntimeError(
f"Received unknown type '{cairo_type}' from parser."
) # pragma: no cover
def _get_struct(self, name: str):
if name not in self.defined_types:
raise UnknownCairoTypeError(name)
return self.defined_types[name]

View File

@@ -0,0 +1,59 @@
from __future__ import annotations
from typing import Dict, Union
from ...abi.v1.parser_transformer import parse
from ..data_types import CairoType, EnumType, StructType, TypeIdentifier
class UnknownCairoTypeError(ValueError):
"""
Error thrown when TypeParser finds type that was not declared prior to parsing.
"""
type_name: str
def __init__(self, type_name: str):
super().__init__(
# pylint: disable=line-too-long
f"Type '{type_name}' is not defined. Please report this issue at https://github.com/software-mansion/starknet.py/issues"
)
self.type_name = type_name
class TypeParser:
"""
Low level utility class for parsing Cairo types that can be used in external methods.
"""
defined_types: Dict[str, Union[StructType, EnumType]]
def __init__(self, defined_types: Dict[str, Union[StructType, EnumType]]):
"""
TypeParser constructor.
:param defined_types: dictionary containing all defined types. For now, they can only be structures.
"""
self.defined_types = defined_types
for name, defined_type in defined_types.items():
if name != defined_type.name:
raise ValueError(
f"Keys must match name of type, '{name}' != '{defined_type.name}'."
)
def parse_inline_type(self, type_string: str) -> CairoType:
"""
Inline type is one that can be used inline, for instance as return type. For instance
(core::felt252, (), (core::felt252,)). Structure can only be referenced in inline type, can't be defined
this way.
:param type_string: type to parse.
"""
parsed = parse(type_string, self.defined_types)
if isinstance(parsed, TypeIdentifier):
for defined_name in self.defined_types.keys():
if parsed.name == defined_name.split("<")[0].strip(":"):
return self.defined_types[defined_name]
raise UnknownCairoTypeError(parsed.name)
return parsed

View File

@@ -0,0 +1,77 @@
from __future__ import annotations
from typing import Dict, Union
from ...abi.v2.parser_transformer import parse
from ..data_types import (
CairoType,
EnumType,
EventType,
StructType,
TypeIdentifier,
)
class UnknownCairoTypeError(ValueError):
"""
Error thrown when TypeParser finds type that was not declared prior to parsing.
"""
type_name: str
def __init__(self, type_name: str):
super().__init__(
# pylint: disable=line-too-long
f"Type '{type_name}' is not defined. Please report this issue at https://github.com/software-mansion/starknet.py/issues"
)
self.type_name = type_name
class TypeParser:
"""
Low level utility class for parsing Cairo types that can be used in external methods.
"""
defined_types: Dict[str, Union[StructType, EnumType, EventType]]
def __init__(
self, defined_types: Dict[str, Union[StructType, EnumType, EventType]]
):
"""
TypeParser constructor.
:param defined_types: dictionary containing all defined types. For now, they can only be structures.
"""
self.defined_types = defined_types
for name, defined_type in defined_types.items():
if name != defined_type.name:
raise ValueError(
f"Keys must match name of type, '{name}' != '{defined_type.name}'."
)
def update_defined_types(
self, defined_types: Dict[str, Union[StructType, EnumType, EventType]]
) -> None:
self.defined_types.update(defined_types)
def add_defined_type(
self, defined_type: Union[StructType, EnumType, EventType]
) -> None:
self.defined_types.update({defined_type.name: defined_type})
def parse_inline_type(self, type_string: str) -> CairoType:
"""
Inline type is one that can be used inline, for instance as return type. For instance
(core::felt252, (), (core::felt252,)). Structure can only be referenced in inline type, can't be defined
this way.
:param type_string: type to parse.
"""
parsed = parse(type_string, self.defined_types)
if isinstance(parsed, TypeIdentifier):
for defined_name in self.defined_types.keys():
if parsed.name == defined_name.split("<")[0].strip(":"):
return self.defined_types[defined_name]
raise UnknownCairoTypeError(parsed.name)
return parsed