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
46 changes: 46 additions & 0 deletions docs/semantic-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Before lowering starts, IRx guarantees that analyzed nodes may carry
- `resolved_imports`
- `resolved_operator`
- `resolved_assignment`
- `resolved_field_access`
- `semantic_flags`
- `extras`

Expand Down Expand Up @@ -78,6 +79,51 @@ Boolean behavior is part of the stable semantic boundary:
Lowering should branch directly on the analyzed Boolean `i1` value for control
flow instead of inventing zero-comparison truthiness rules during codegen.

## Struct Contract

Structs are IRx's stable composite storage and ABI foundation.

- struct names are stable semantic symbols
- field order is exactly declaration order
- field names must be unique within a struct
- field types must resolve semantically before lowering
- field layout must not be implicitly reordered by semantics or lowering
- field access must resolve semantically before codegen and lower by stable
field index
- nested structs by value are allowed when every referenced struct is fully
defined
- direct by-value recursive structs are forbidden
- mutual by-value recursive structs are forbidden
- structs can be passed and returned by value within IRx-defined functions
- emitted LLVM struct types are plain data with no hidden headers, metadata,
tags, or runtime object payloads

For now, empty structs are rejected explicitly instead of relying on backend-
specific behavior.

Example scalar wrapper:

```python
astx.StructDefStmt(
name="ScalarBox",
attributes=[
astx.VariableDeclaration(name="value", type_=astx.Int32()),
],
)
```

Example nested record:

```python
astx.StructDefStmt(
name="Descriptor",
attributes=[
astx.VariableDeclaration(name="point", type_=astx.StructType("Point")),
astx.VariableDeclaration(name="ready", type_=astx.Boolean()),
],
)
```

### Canonical Cast Policy

Implicit promotions in variable initializers, assignments, call arguments, and
Expand Down
1 change: 1 addition & 0 deletions src/irx/analysis/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class SemanticContract:
"resolved_imports",
"resolved_operator",
"resolved_assignment",
"resolved_field_access",
"semantic_flags",
"extras",
),
Expand Down
204 changes: 204 additions & 0 deletions src/irx/analysis/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from irx.analysis.registry import SemanticRegistry
from irx.analysis.resolved_nodes import (
ResolvedAssignment,
ResolvedFieldAccess,
ResolvedImportBinding,
ResolvedOperator,
SemanticFlags,
Expand Down Expand Up @@ -220,6 +221,86 @@ def _set_assignment(
"""
raise NotImplementedError

def _set_field_access(
self,
node: astx.AST,
field_access: ResolvedFieldAccess | None,
) -> None:
"""
title: Attach resolved field access metadata.
parameters:
node:
type: astx.AST
field_access:
type: ResolvedFieldAccess | None
"""
raise NotImplementedError

def _resolve_struct_from_type(
self,
type_: astx.DataType | None,
*,
node: astx.AST,
unknown_message: str,
) -> SemanticStruct | None:
"""
title: Resolve one struct-valued type reference.
parameters:
type_:
type: astx.DataType | None
node:
type: astx.AST
unknown_message:
type: str
returns:
type: SemanticStruct | None
"""
raise NotImplementedError

def _resolve_declared_type(
self,
type_: astx.DataType,
*,
node: astx.AST,
unknown_message: str = "Unknown type '{name}'",
) -> astx.DataType:
"""
title: Resolve one declared type in place.
parameters:
type_:
type: astx.DataType
node:
type: astx.AST
unknown_message:
type: str
returns:
type: astx.DataType
"""
raise NotImplementedError

def _root_assignment_symbol(
self,
node: astx.AST | None,
) -> SemanticSymbol | None:
"""
title: Resolve the root symbol for an assignment target chain.
parameters:
node:
type: astx.AST | None
returns:
type: SemanticSymbol | None
"""
raise NotImplementedError

def _predeclare_block_structs(self, block: astx.Block) -> None:
"""
title: Predeclare struct definitions in one block.
parameters:
block:
type: astx.Block
"""
raise NotImplementedError

def _current_module_key(self) -> ModuleKey:
"""
title: Return the current module key.
Expand Down Expand Up @@ -548,6 +629,129 @@ def _set_assignment(
return
info.resolved_assignment = ResolvedAssignment(symbol)

def _set_field_access(
self,
node: astx.AST,
field_access: ResolvedFieldAccess | None,
) -> None:
"""
title: Attach resolved field access metadata.
parameters:
node:
type: astx.AST
field_access:
type: ResolvedFieldAccess | None
"""
self._semantic(node).resolved_field_access = field_access

def _resolve_struct_from_type(
self,
type_: astx.DataType | None,
*,
node: astx.AST,
unknown_message: str,
) -> SemanticStruct | None:
"""
title: Resolve one struct-valued type reference.
parameters:
type_:
type: astx.DataType | None
node:
type: astx.AST
unknown_message:
type: str
returns:
type: SemanticStruct | None
"""
if not isinstance(type_, astx.StructType):
return None

binding = self.bindings.resolve(type_.name)
struct = (
binding.struct
if binding is not None and binding.kind == "struct"
else None
)
if struct is None and type_.module_key is not None:
lookup_name = type_.resolved_name or type_.name
struct = self.context.get_struct(type_.module_key, lookup_name)
if struct is None:
self.context.diagnostics.add(
unknown_message.format(name=type_.name),
node=node,
)
return None

type_.resolved_name = struct.name
type_.module_key = struct.module_key
type_.qualified_name = struct.qualified_name
self._set_struct(type_, struct)
self._set_type(type_, type_)
return struct

def _resolve_declared_type(
self,
type_: astx.DataType,
*,
node: astx.AST,
unknown_message: str = "Unknown type '{name}'",
) -> astx.DataType:
"""
title: Resolve one declared type in place.
parameters:
type_:
type: astx.DataType
node:
type: astx.AST
unknown_message:
type: str
returns:
type: astx.DataType
"""
self._resolve_struct_from_type(
type_,
node=node,
unknown_message=unknown_message,
)
return type_

def _root_assignment_symbol(
self,
node: astx.AST | None,
) -> SemanticSymbol | None:
"""
title: Resolve the root symbol for an assignment target chain.
parameters:
node:
type: astx.AST | None
returns:
type: SemanticSymbol | None
"""
if node is None:
return None
if isinstance(node, astx.Identifier):
return cast(
SemanticInfo,
getattr(node, "semantic", SemanticInfo()),
).resolved_symbol
if isinstance(node, astx.FieldAccess):
return self._root_assignment_symbol(node.value)
return None

def _predeclare_block_structs(self, block: astx.Block) -> None:
"""
title: Predeclare struct definitions in one block.
parameters:
block:
type: astx.Block
"""
for node in block.nodes:
if not isinstance(node, astx.StructDefStmt):
continue
struct = self.registry.register_struct(node)
self.bindings.bind_struct(node.name, struct, node=node)
self._set_struct(node, struct)

def _current_module_key(self) -> ModuleKey:
"""
title: Return the current module key.
Expand Down
Loading
Loading