blob: fa284c44eaf795bc99ae97f2c82dbbcee9fd6c3e [file] [log] [blame]
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Intermediate representation (IR) for Emboss.
This is limited to purely data and type annotations.
"""
import dataclasses
import enum
import sys
from typing import ClassVar, Optional
from compiler.util import ir_data_fields
@dataclasses.dataclass
class Message:
"""Base class for IR data objects.
Historically protocol buffers were used for serializing this data which has
led to some legacy naming conventions and references. In particular this
class is named `Message` in the sense of a protocol buffer message,
indicating that it is intended to just be data that is used by other higher
level services.
There are some other legacy idioms leftover from the protocol buffer-based
definition such as support for "oneof" and optional fields.
"""
IR_DATACLASS: ClassVar[object] = object()
field_specs: ClassVar[ir_data_fields.FilteredIrFieldSpecs]
def __post_init__(self):
"""Called by dataclass subclasses after init.
Post-processes any lists passed in to use our custom list type.
"""
# Convert any lists passed in to CopyValuesList
for spec in self.field_specs.sequence_field_specs:
cur_val = getattr(self, spec.name)
if isinstance(cur_val, ir_data_fields.TemporaryCopyValuesList):
copy_val = cur_val.temp_list
else:
copy_val = ir_data_fields.CopyValuesList(spec.data_type)
if cur_val:
copy_val.shallow_copy(cur_val)
setattr(self, spec.name, copy_val)
# This hook adds a 15% overhead to end-to-end code generation in some cases
# so we guard it in a `__debug__` block. Users can opt-out of this check by
# running python with the `-O` flag, ie: `python3 -O ./embossc`.
if __debug__:
def __setattr__(self, name: str, value) -> None:
"""Debug-only hook that adds basic type checking for ir_data fields."""
if spec := self.field_specs.all_field_specs.get(name):
if (
# Check if it's the expected type
not isinstance(value, spec.data_type) and
# Oneof fields are a special case
not spec.is_oneof and
# Optional fields can be set to None
not (spec.container is ir_data_fields.FieldContainer.OPTIONAL and
value is None) and
# Sequences can be a few variants of lists
not (spec.is_sequence and
isinstance(value, (
list, ir_data_fields.TemporaryCopyValuesList,
ir_data_fields.CopyValuesList))) and
# An enum value can be an int
not (spec.is_enum and isinstance(value, int))):
raise AttributeError(
f"Cannot set {value} (type {value.__class__}) for type"
"{spec.data_type}")
object.__setattr__(self, name, value)
# Non-PEP8 name to mimic the Google Protobuf interface.
def HasField(self, name): # pylint:disable=invalid-name
"""Indicates if this class has the given field defined and it is set."""
return getattr(self, name, None) is not None
# Non-PEP8 name to mimic the Google Protobuf interface.
def WhichOneof(self, oneof_name): # pylint:disable=invalid-name
"""Indicates which field has been set for the oneof value.
Returns None if no field has been set.
"""
for field_name, oneof in self.field_specs.oneof_mappings:
if oneof == oneof_name and self.HasField(field_name):
return field_name
return None
################################################################################
# From here to the end of the file are actual structure definitions.
@dataclasses.dataclass
class Position(Message):
"""A zero-width position within a source file."""
line: int = 0
"""Line (starts from 1)."""
column: int = 0
"""Column (starts from 1)."""
@dataclasses.dataclass
class Location(Message):
"""A half-open start:end range within a source file."""
start: Optional[Position] = None
"""Beginning of the range"""
end: Optional[Position] = None
"""One column past the end of the range."""
is_disjoint_from_parent: Optional[bool] = None
"""True if this Location is outside of the parent object's Location."""
is_synthetic: Optional[bool] = None
"""True if this Location's parent was synthesized, and does not directly
appear in the source file.
The Emboss front end uses this field to cull
irrelevant error messages.
"""
@dataclasses.dataclass
class Word(Message):
"""IR for a bare word in the source file.
This is used in NameDefinitions and References.
"""
text: Optional[str] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class String(Message):
"""IR for a string in the source file."""
text: Optional[str] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class Documentation(Message):
text: Optional[str] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class BooleanConstant(Message):
"""IR for a boolean constant."""
value: Optional[bool] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class Empty(Message):
"""Placeholder message for automatic element counts for arrays."""
source_location: Optional[Location] = None
@dataclasses.dataclass
class NumericConstant(Message):
"""IR for any numeric constant."""
# Numeric constants are stored as decimal strings; this is the simplest way
# to store the full -2**63..+2**64 range.
#
# TODO(bolms): switch back to int, and just use strings during
# serialization, now that we're free of proto.
value: Optional[str] = None
source_location: Optional[Location] = None
class FunctionMapping(int, enum.Enum):
"""Enum of supported function types"""
UNKNOWN = 0
ADDITION = 1
"""`+`"""
SUBTRACTION = 2
"""`-`"""
MULTIPLICATION = 3
"""`*`"""
EQUALITY = 4
"""`==`"""
INEQUALITY = 5
"""`!=`"""
AND = 6
"""`&&`"""
OR = 7
"""`||`"""
LESS = 8
"""`<`"""
LESS_OR_EQUAL = 9
"""`<=`"""
GREATER = 10
"""`>`"""
GREATER_OR_EQUAL = 11
"""`>=`"""
CHOICE = 12
"""`?:`"""
MAXIMUM = 13
"""`$max()`"""
PRESENCE = 14
"""`$present()`"""
UPPER_BOUND = 15
"""`$upper_bound()`"""
LOWER_BOUND = 16
"""`$lower_bound()`"""
@dataclasses.dataclass
class Function(Message):
"""IR for a single function (+, -, *, ==, $max, etc.) in an expression."""
function: Optional[FunctionMapping] = None
args: list["Expression"] = ir_data_fields.list_field(lambda: Expression)
function_name: Optional[Word] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class CanonicalName(Message):
"""CanonicalName is the unique, absolute name for some object.
A CanonicalName is the unique, absolute name for some object (Type, field,
etc.) in the IR. It is used both in the definitions of objects ("struct
Foo"), and in references to objects (a field of type "Foo").
"""
module_file: str = ir_data_fields.str_field()
"""The module_file is the Module.source_file_name of the Module in which this
object's definition appears.
Note that the Prelude always has a Module.source_file_name of "", and thus
references to Prelude names will have module_file == "".
"""
object_path: list[str] = ir_data_fields.list_field(str)
"""The object_path is the canonical path to the object definition within its
module file.
For example, the field "bar" would have an object path of
["Foo", "bar"]:
struct Foo:
0:3 UInt bar
The enumerated name "BOB" would have an object path of ["Baz", "Qux",
"BOB"]:
struct Baz:
0:3 Qux qux
enum Qux:
BOB = 0
"""
@dataclasses.dataclass
class NameDefinition(Message):
"""NameDefinition is IR for the name of an object, within the object.
That is, a TypeDefinition or Field will hold a NameDefinition as its
name.
"""
name: Optional[Word] = None
"""The name, as directly generated from the source text.
name.text will match the last element of canonical_name.object_path. Note
that in some cases, the exact string in name.text may not appear in the
source text.
"""
canonical_name: Optional[CanonicalName] = None
"""The CanonicalName that will appear in References.
This field is technically redundant: canonical_name.module_file should always
match the source_file_name of the enclosing Module, and
canonical_name.object_path should always match the names of parent nodes.
"""
is_anonymous: Optional[bool] = None
"""If true, indicates that this is an automatically-generated name, which
should not be visible outside of its immediate namespace.
"""
source_location: Optional[Location] = None
"""The location of this NameDefinition in source code."""
@dataclasses.dataclass
class Reference(Message):
"""A Reference holds the canonical name of something defined elsewhere.
For example, take this fragment:
struct Foo:
0:3 UInt size (s)
4:s Int:8[] payload
"Foo", "size", and "payload" will become NameDefinitions in their
corresponding Field and Message IR objects, while "UInt", the second "s",
and "Int" are References. Note that the second "s" will have a
canonical_name.object_path of ["Foo", "size"], not ["Foo", "s"]: the
Reference always holds the single "true" name of the object, regardless of
what appears in the .emb.
"""
canonical_name: Optional[CanonicalName] = None
"""The canonical name of the object being referred to.
This name should be used to find the object in the IR.
"""
source_name: list[Word] = ir_data_fields.list_field(Word)
"""The source_name is the name the user entered in the source file.
The source_name could be either relative or absolute, and may be an alias
(and thus not match any part of the canonical_name). Back ends should use
canonical_name for name lookup, and reserve source_name for error messages.
"""
is_local_name: Optional[bool] = None
"""If true, then symbol resolution should only look at local names when
resolving source_name.
This is used so that the names of inline types aren't "ambiguous" if there
happens to be another type with the same name at a parent scope.
"""
# TODO(bolms): Allow absolute paths starting with ".".
source_location: Optional[Location] = None
"""Note that this is the source_location of the *Reference*, not of the
object to which it refers.
"""
@dataclasses.dataclass
class FieldReference(Message):
"""IR for a "field" or "field.sub.subsub" reference in an expression.
The first element of "path" is the "base" field, which should be directly
readable in the (runtime) context of the expression. For example:
struct Foo:
0:1 UInt header_size (h)
0:h UInt:8[] header_bytes
The "h" will translate to ["Foo", "header_size"], which will be the first
(and in this case only) element of "path".
Subsequent path elements should be treated as subfields. For example, in:
struct Foo:
struct Sizes:
0:1 UInt header_size
1:2 UInt body_size
0 [+2] Sizes sizes
0 [+sizes.header_size] UInt:8[] header
sizes.header_size [+sizes.body_size] UInt:8[] body
The references to "sizes.header_size" will have a path of [["Foo",
"sizes"], ["Foo", "Sizes", "header_size"]]. Note that each path element is
a fully-qualified reference; some back ends (C++, Python) may only use the
last element, while others (C) may use the complete path.
This representation is a bit awkward, and is fundamentally limited to a
dotted list of static field names. It does not allow an expression like
`array[n]` on the left side of a `.`. At this point, it is an artifact of
the era during which I (bolms@) thought I could get away with skipping
compiler-y things.
"""
# TODO(bolms): Add composite types to the expression type system, and
# replace FieldReference with a "member access" Expression kind. Further,
# move the symbol resolution for FieldReferences that is currently in
# symbol_resolver.py into type_check.py.
# TODO(bolms): Make the above change before declaring the IR to be "stable".
path: list[Reference] = ir_data_fields.list_field(Reference)
source_location: Optional[Location] = None
@dataclasses.dataclass
class OpaqueType(Message):
pass
@dataclasses.dataclass
class IntegerType(Message):
"""Type of an integer expression."""
# For optimization, the modular congruence of an integer expression is
# tracked. This consists of a modulus and a modular_value, such that for
# all possible values of expression, expression MOD modulus ==
# modular_value.
#
# The modulus may be the special value "infinity" to indicate that the
# expression's value is exactly modular_value; otherwise, it should be a
# positive integer.
#
# A modulus of 1 places no constraints on the value.
#
# The modular_value should always be a nonnegative integer that is smaller
# than the modulus.
#
# Note that this is specifically the *modulus*, which is not equivalent to
# the value from C's '%' operator when the dividend is negative: in C, -7 %
# 4 == -3, but the modular_value here would be 1. Python uses modulus: in
# Python, -7 % 4 == 1.
modulus: Optional[str] = None
"""The modulus portion of the modular congruence of an integer expression.
The modulus may be the special value "infinity" to indicate that the
expression's value is exactly modular_value; otherwise, it should be a
positive integer.
A modulus of 1 places no constraints on the value.
"""
modular_value: Optional[str] = None
""" The modular_value portion of the modular congruence of an integer expression.
The modular_value should always be a nonnegative integer that is smaller
than the modulus.
"""
# The minimum and maximum values of an integer are tracked and checked so
# that Emboss can implement reliable arithmetic with no operations
# overflowing either 64-bit unsigned or 64-bit signed 2's-complement
# integers.
#
# Note that constant subexpressions are allowed to overflow, as long as the
# final, computed constant value of the subexpression fits in a 64-bit
# value.
#
# The minimum_value may take the value "-infinity", and the maximum_value
# may take the value "infinity". These sentinel values indicate that
# Emboss has no bound information for the Expression, and therefore the
# Expression may only be evaluated during compilation; the back end should
# never need to compile such an expression into the target language (e.g.,
# C++).
minimum_value: Optional[str] = None
maximum_value: Optional[str] = None
@dataclasses.dataclass
class BooleanType(Message):
value: Optional[bool] = None
@dataclasses.dataclass
class EnumType(Message):
name: Optional[Reference] = None
value: Optional[str] = None
@dataclasses.dataclass
class ExpressionType(Message):
opaque: Optional[OpaqueType] = ir_data_fields.oneof_field("type")
integer: Optional[IntegerType] = ir_data_fields.oneof_field("type")
boolean: Optional[BooleanType] = ir_data_fields.oneof_field("type")
enumeration: Optional[EnumType] = ir_data_fields.oneof_field("type")
@dataclasses.dataclass
class Expression(Message):
"""IR for an expression.
An Expression is a potentially-recursive data structure. It can either
represent a leaf node (constant or reference) or an operation combining
other Expressions (function).
"""
constant: Optional[NumericConstant] = ir_data_fields.oneof_field("expression")
constant_reference: Optional[Reference] = ir_data_fields.oneof_field(
"expression"
)
function: Optional[Function] = ir_data_fields.oneof_field("expression")
field_reference: Optional[FieldReference] = ir_data_fields.oneof_field(
"expression"
)
boolean_constant: Optional[BooleanConstant] = ir_data_fields.oneof_field(
"expression"
)
builtin_reference: Optional[Reference] = ir_data_fields.oneof_field(
"expression"
)
type: Optional[ExpressionType] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class ArrayType(Message):
"""IR for an array type ("Int:8[12]" or "Message[2]" or "UInt[3][2]")."""
base_type: Optional["Type"] = None
element_count: Optional[Expression] = ir_data_fields.oneof_field("size")
automatic: Optional[Empty] = ir_data_fields.oneof_field("size")
source_location: Optional[Location] = None
@dataclasses.dataclass
class AtomicType(Message):
"""IR for a non-array type ("UInt" or "Foo(Version.SIX)")."""
reference: Optional[Reference] = None
runtime_parameter: list[Expression] = ir_data_fields.list_field(Expression)
source_location: Optional[Location] = None
@dataclasses.dataclass
class Type(Message):
"""IR for a type reference ("UInt", "Int:8[12]", etc.)."""
atomic_type: Optional[AtomicType] = ir_data_fields.oneof_field("type")
array_type: Optional[ArrayType] = ir_data_fields.oneof_field("type")
size_in_bits: Optional[Expression] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class AttributeValue(Message):
"""IR for a attribute value."""
# TODO(bolms): Make String a type of Expression, and replace
# AttributeValue with Expression.
expression: Optional[Expression] = ir_data_fields.oneof_field("value")
string_constant: Optional[String] = ir_data_fields.oneof_field("value")
source_location: Optional[Location] = None
@dataclasses.dataclass
class Attribute(Message):
"""IR for a [name = value] attribute."""
name: Optional[Word] = None
value: Optional[AttributeValue] = None
back_end: Optional[Word] = None
is_default: Optional[bool] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class WriteTransform(Message):
"""IR which defines an expression-based virtual field write scheme.
E.g., for a virtual field like `x_plus_one`:
struct Foo:
0 [+1] UInt x
let x_plus_one = x + 1
... the `WriteMethod` would be `transform`, with `$logical_value - 1` for
`function_body` and `x` for `destination`.
"""
function_body: Optional[Expression] = None
destination: Optional[FieldReference] = None
@dataclasses.dataclass
class WriteMethod(Message):
"""IR which defines the method used for writing to a virtual field."""
physical: Optional[bool] = ir_data_fields.oneof_field("method")
"""A physical Field can be written directly."""
read_only: Optional[bool] = ir_data_fields.oneof_field("method")
"""A read_only Field cannot be written."""
alias: Optional[FieldReference] = ir_data_fields.oneof_field("method")
"""An alias is a direct, untransformed forward of another field; it can be
implemented by directly returning a reference to the aliased field.
Aliases are the only kind of virtual field that may have an opaque type.
"""
transform: Optional[WriteTransform] = ir_data_fields.oneof_field("method")
"""A transform is a way of turning a logical value into a value which should
be written to another field.
A virtual field like `let y = x + 1` would
have a transform WriteMethod to subtract 1 from the new `y` value, and
write that to `x`.
"""
@dataclasses.dataclass
class FieldLocation(Message):
"""IR for a field location."""
start: Optional[Expression] = None
size: Optional[Expression] = None
source_location: Optional[Location] = None
@dataclasses.dataclass
class Field(Message): # pylint:disable=too-many-instance-attributes
"""IR for a field in a struct definition.
There are two kinds of Field: physical fields have location and (physical)
type; virtual fields have read_transform. Although there are differences,
in many situations physical and virtual fields are treated the same way,
and they can be freely intermingled in the source file.
"""
location: Optional[FieldLocation] = None
"""The physical location of the field."""
type: Optional[Type] = None
"""The physical type of the field."""
read_transform: Optional[Expression] = None
"""The value of a virtual field."""
write_method: Optional[WriteMethod] = None
"""How this virtual field should be written."""
name: Optional[NameDefinition] = None
"""The name of the field."""
abbreviation: Optional[Word] = None
"""An optional short name for the field, only visible inside the enclosing bits/struct."""
attribute: list[Attribute] = ir_data_fields.list_field(Attribute)
"""Field-specific attributes."""
documentation: list[Documentation] = ir_data_fields.list_field(Documentation)
"""Field-specific documentation."""
# TODO(bolms): Document conditional fields better, and replace some of this
# explanation with a reference to the documentation.
existence_condition: Optional[Expression] = None
"""The field only exists when existence_condition evaluates to true.
For example:
```
struct Message:
0 [+4] UInt length
4 [+8] MessageType message_type
if message_type == MessageType.FOO:
8 [+length] Foo foo
if message_type == MessageType.BAR:
8 [+length] Bar bar
8+length [+4] UInt crc
```
For `length`, `message_type`, and `crc`, existence_condition will be
`boolean_constant { value: true }`
For `foo`, existence_condition will be:
```
function { function: EQUALITY
args: [reference to message_type]
args: { [reference to MessageType.FOO] } }
```
The `bar` field will have a similar existence_condition to `foo`:
```
function { function: EQUALITY
args: [reference to message_type]
args: { [reference to MessageType.BAR] } }
```
When `message_type` is `MessageType.BAR`, the `Message` struct does not contain
field `foo`, and vice versa for `message_type == MessageType.FOO` and field
`bar`: those fields only conditionally exist in the structure.
"""
source_location: Optional[Location] = None
@dataclasses.dataclass
class Structure(Message):
"""IR for a bits or struct definition."""
field: list[Field] = ir_data_fields.list_field(Field)
fields_in_dependency_order: list[int] = ir_data_fields.list_field(int)
"""The fields in `field` are listed in the order they appear in the original
.emb.
For text format output, this can lead to poor results. Take the following
struct:
```
struct Foo:
b [+4] UInt a
0 [+4] UInt b
```
Here, the location of `a` depends on the current value of `b`. Because of
this, if someone calls
```
emboss::UpdateFromText(foo_view, "{ a: 10, b: 4 }");
```
then foo_view will not be updated the way one would expect: if `b`'s value
was something other than 4 to start with, then `UpdateFromText` will write
the 10 to some other location, then update `b` to 4.
To avoid surprises, `emboss::DumpAsText` should return `"{ b: 4, a: 10
}"`.
The `fields_in_dependency_order` field provides a permutation of `field`
such that each field appears after all of its dependencies. For example,
`struct Foo`, above, would have `{ 1, 0 }` in
`fields_in_dependency_order`.
The exact ordering of `fields_in_dependency_order` is not guaranteed, but
some effort is made to keep the order close to the order fields are listed
in the original `.emb` file. In particular, if the ordering 0, 1, 2, 3,
... satisfies dependency ordering, then `fields_in_dependency_order` will
be `{ 0, 1, 2, 3, ... }`.
"""
source_location: Optional[Location] = None
@dataclasses.dataclass
class External(Message):
"""IR for an external type declaration."""
# Externals have no values other than name and attribute list, which are
# common to all type definitions.
source_location: Optional[Location] = None
@dataclasses.dataclass
class EnumValue(Message):
"""IR for a single value within an enumerated type."""
name: Optional[NameDefinition] = None
"""The name of the enum value."""
value: Optional[Expression] = None
"""The value of the enum value."""
documentation: list[Documentation] = ir_data_fields.list_field(Documentation)
"""Value-specific documentation."""
attribute: list[Attribute] = ir_data_fields.list_field(Attribute)
"""Value-specific attributes."""
source_location: Optional[Location] = None
@dataclasses.dataclass
class Enum(Message):
"""IR for an enumerated type definition."""
value: list[EnumValue] = ir_data_fields.list_field(EnumValue)
source_location: Optional[Location] = None
@dataclasses.dataclass
class Import(Message):
"""IR for an import statement in a module."""
file_name: Optional[String] = None
"""The file to import."""
local_name: Optional[Word] = None
"""The name to use within this module."""
source_location: Optional[Location] = None
@dataclasses.dataclass
class RuntimeParameter(Message):
"""IR for a runtime parameter definition."""
name: Optional[NameDefinition] = None
"""The name of the parameter."""
type: Optional[ExpressionType] = None
"""The type of the parameter."""
# TODO(bolms): Actually implement the set builder type notation.
physical_type_alias: Optional[Type] = None
"""For convenience and readability, physical types may be used in the .emb
source instead of a full expression type.
That way, users can write
something like:
```
struct Foo(version :: UInt:8):
```
instead of:
```
struct Foo(version :: {$int x |: 0 <= x <= 255}):
```
In these cases, physical_type_alias holds the user-supplied type, and type
is filled in after initial parsing is finished.
"""
source_location: Optional[Location] = None
class AddressableUnit(int, enum.Enum):
"""The "addressable unit" is the size of the smallest unit that can be read
from the backing store that this type expects. For `struct`s, this is
BYTE; for `enum`s and `bits`, this is BIT, and for `external`s it depends
on the specific type
"""
NONE = 0
BIT = 1
BYTE = 8
@dataclasses.dataclass
class TypeDefinition(Message):
"""Container IR for a type definition (struct, union, etc.)"""
external: Optional[External] = ir_data_fields.oneof_field("type")
enumeration: Optional[Enum] = ir_data_fields.oneof_field("type")
structure: Optional[Structure] = ir_data_fields.oneof_field("type")
name: Optional[NameDefinition] = None
"""The name of the type."""
attribute: list[Attribute] = ir_data_fields.list_field(Attribute)
"""All attributes attached to the type."""
documentation: list[Documentation] = ir_data_fields.list_field(Documentation)
"""Docs for the type."""
# pylint:disable=undefined-variable
subtype: list["TypeDefinition"] = ir_data_fields.list_field(
lambda: TypeDefinition
)
"""Subtypes of this type."""
addressable_unit: Optional[AddressableUnit] = None
runtime_parameter: list[RuntimeParameter] = ir_data_fields.list_field(
RuntimeParameter
)
"""If the type requires parameters at runtime, these are its parameters.
These are currently only allowed on structures, but in the future they
should be allowed on externals.
"""
source_location: Optional[Location] = None
@dataclasses.dataclass
class Module(Message):
"""The IR for an individual Emboss module (file)."""
attribute: list[Attribute] = ir_data_fields.list_field(Attribute)
"""Module-level attributes."""
type: list[TypeDefinition] = ir_data_fields.list_field(TypeDefinition)
"""Module-level type definitions."""
documentation: list[Documentation] = ir_data_fields.list_field(Documentation)
"""Module-level docs."""
foreign_import: list[Import] = ir_data_fields.list_field(Import)
"""Other modules imported."""
source_text: Optional[str] = None
"""The original source code."""
source_location: Optional[Location] = None
"""Source code covered by this IR."""
source_file_name: Optional[str] = None
"""Name of the source file."""
@dataclasses.dataclass
class EmbossIr(Message):
"""The top-level IR for an Emboss module and all of its dependencies."""
module: list[Module] = ir_data_fields.list_field(Module)
"""All modules.
The first entry will be the main module; back ends should
generate code corresponding to that module. The second entry will be the
prelude module.
"""
# Post-process the dataclasses to add cached fields.
ir_data_fields.cache_message_specs(sys.modules[Message.__module__], Message)