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
17 changes: 12 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,21 @@ API.

## Runtime Type Checking

- Use `irx.typecheck.typechecked` on every concrete class defined under
`src/irx/`.
- Keep `@public` or `@private` outermost, then `@typechecked`, then
- Use `irx.typecheck.typechecked` on every module-level function and every
concrete class defined under `src/irx/`.
- Class-level `@typechecked` is the default way to cover methods. Do not add
per-method decorators just to mirror the policy unless a class cannot be
decorated, and if that ever happens document the reason.
- For functions, keep `@public` or `@private` outermost and place `@typechecked`
on the implementation boundary. Usually that means directly under `@public` or
`@private`; for wrappers like `@lru_cache(...)`, keep `@typechecked` closest
to the original function so Typeguard can instrument it.
- For classes, keep `@public` or `@private` outermost, then `@typechecked`, then
`@dataclass(...)` so generated dataclass methods are instrumented.
- Exempt only `Protocol` definitions and type-checking-only helper stubs that
are intentionally kept out of the runtime MRO.
- If a concrete class truly needs an exemption, document the reason and update
`tests/test_typechecked_policy.py` in the same change.
- If a function or concrete class truly needs an exemption, document the reason
and update `tests/test_typechecked_policy.py` in the same change.

## Working In `irx.builder`

Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ via `clang`.
- **Literals:** `LiteralInt16`, `LiteralInt32`, `LiteralString`
- **Variables:** `Variable`, `VariableDeclaration`,
`InlineVariableDeclaration`
- **Ops:** `UnaryOp` (`++`, `--`), `BinaryOp` (`+ - * / < >`) with simple type
promotion
- **Ops:** `UnaryOp` (`++`, `--`), `BinaryOp` (`+ - * / < >`) with documented
scalar numeric promotion and cast rules
- **Flow:** `IfStmt`, `ForCountLoopStmt`, `ForRangeLoopStmt`
- **Functions:** `FunctionPrototype`, `Function`, `FunctionReturn`,
`FunctionCall`
Expand Down Expand Up @@ -162,6 +162,19 @@ The current MVP is intentionally narrow: primitive `int32` arrays, lifecycle
operations, inspection, and C Data roundtrip support. No full Arrow container
semantics are encoded directly in LLVM IR.

## Scalar Numeric Semantics

IRx now treats scalar numerics as a stable substrate instead of an ad hoc
"simple promotion" layer:

- one canonical promotion table for signed integers, unsigned integers, and
floats
- one canonical implicit-promotion vs explicit-cast policy
- comparisons always resolve to `Boolean` / LLVM `i1`

The full contract lives in
[docs/semantic-contract.md](https://github.com/arxlang/irx/blob/main/docs/semantic-contract.md).

## Testing

```bash
Expand Down
14 changes: 14 additions & 0 deletions contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ makim tests.linter
makim tests.unittest
```

## Runtime type checking

- Use `irx.typecheck.typechecked` on every module-level function and every
concrete class under `src/irx`.
- Class decorators are the default way to cover methods; do not add per-method
decorators unless a class cannot be decorated.
- Keep `@public` or `@private` outermost and place `@typechecked` on the
implementation boundary; for wrappers like `@lru_cache(...)`, that means
keeping `@typechecked` closest to the original function.
- Keep class decorators ordered as `@public` or `@private`, then `@typechecked`,
then `@dataclass(...)`.
- Run `pytest tests/test_typechecked_policy.py -q` when you touch decorator
coverage or add an exemption.

## Full guidelines

Please see the full contributing guide for project layout, workflow, and release
Expand Down
16 changes: 16 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ Ready to contribute? Here’s how to set up `irx` for local development.
$ makim tests.unittest
```

## Runtime Type Checking

IRx keeps runtime type checking on by default for its own code under `src/irx`.

- Use `irx.typecheck.typechecked` on every module-level function and every
concrete class.
- Methods are expected to be covered through the class decorator; avoid adding
per-method decorators unless the class itself cannot be decorated.
- Keep `@public` or `@private` outermost and place `@typechecked` on the
implementation boundary; for wrappers like `@lru_cache(...)`, that means
keeping `@typechecked` closest to the original function.
- Keep class decorators ordered as `@public` or `@private`, then `@typechecked`,
then `@dataclass(...)`.
- If you need an exemption for a `Protocol` or a typing-only stub, document it
clearly and update `tests/test_typechecked_policy.py` in the same change.

6. Commit your changes and push your branch to GitHub:

```bash
Expand Down
40 changes: 40 additions & 0 deletions docs/semantic-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,46 @@ For multi-module compilation, IRx also guarantees the following
Lowering should consume this semantic metadata instead of re-deriving meaning
from raw syntax.

## Scalar Numeric Foundation

Binary scalar numerics use one canonical promotion table:

| Operand mix | Promoted operand type |
| --------------------- | ---------------------------------------------------------------------------------------------------- |
| `float + float` | wider float |
| `float + integer` | float widened to cover the integer width floor (`16`, `32`, or `64` bits), capped at `Float64` |
| `signed + signed` | wider signed integer |
| `unsigned + unsigned` | wider unsigned integer |
| `signed + unsigned` | wider signed integer when the signed operand is strictly wider; otherwise the wider unsigned integer |

Comparison operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) promote their operands
with the same table and always return `Boolean` semantically and `i1` in LLVM
IR.

### Canonical Cast Policy

Implicit promotions in variable initializers, assignments, call arguments, and
returns are intentionally narrower than explicit casts:

- same-type assignment is always allowed
- signed integers may widen to wider signed integers
- unsigned integers may widen to wider unsigned integers
- unsigned integers may widen to strictly wider signed integers
- integers may promote to floats when the target float width meets the same
`16`/`32`/`64` floor used by the numeric-promotion table
- floats may widen to wider floats
- implicit sign-changing integer casts to unsigned targets are rejected
- implicit narrowing casts are rejected
- implicit float-to-integer and numeric-to-boolean casts are rejected

Explicit `Cast(...)` expressions allow the full scalar conversions:

- numeric-to-numeric casts
- boolean-to-numeric casts using `0` and `1`
- numeric-to-boolean casts using `!= 0` or `!= 0.0`
- string-to-string casts
- numeric/boolean-to-string casts through runtime formatting

## Error Boundaries

- Semantic errors: invalid programs, unsupported semantic input, and import
Expand Down
3 changes: 3 additions & 0 deletions src/irx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

from importlib import metadata as importlib_metadata

from irx.typecheck import typechecked


@typechecked
def get_version() -> str:
"""
title: Return the program version.
Expand Down
4 changes: 4 additions & 0 deletions src/irx/analysis/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ParsedModule,
)
from irx.analysis.session import CompilationSession
from irx.typecheck import typechecked

__all__ = [
"SemanticAnalyzer",
Expand All @@ -29,6 +30,7 @@


@public
@typechecked
def analyze(node: astx.AST) -> astx.AST:
"""
title: Analyze one AST root and attach semantic sidecars.
Expand All @@ -46,6 +48,7 @@ def analyze(node: astx.AST) -> astx.AST:


@public
@typechecked
def analyze_module(module: astx.Module) -> astx.Module:
"""
title: Analyze an AST module.
Expand All @@ -62,6 +65,7 @@ def analyze_module(module: astx.Module) -> astx.Module:


@public
@typechecked
def analyze_modules(
root: ParsedModule,
resolver: ImportResolver,
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 @@ -233,6 +233,7 @@ class SemanticContract:


@public
@typechecked
def get_semantic_contract() -> SemanticContract:
"""
title: Return the stable public semantic contract.
Expand Down
10 changes: 10 additions & 0 deletions src/irx/analysis/module_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
from public import public

from irx.analysis.module_interfaces import ModuleKey
from irx.typecheck import typechecked

_SEGMENT_RE = re.compile(r"[A-Za-z0-9_]+")


@typechecked
def _split_segments(value: str) -> list[str]:
"""
title: Split a string into LLVM-friendly segments.
Expand All @@ -33,6 +35,7 @@ def _split_segments(value: str) -> list[str]:
return [f"x{ord(char):02x}" for char in value]


@typechecked
def _mangle_parts(*parts: str) -> str:
"""
title: Mangle string parts into a deterministic LLVM name.
Expand All @@ -50,6 +53,7 @@ def _mangle_parts(*parts: str) -> str:


@public
@typechecked
def function_key(module_key: ModuleKey, name: str) -> tuple[ModuleKey, str]:
"""
title: Return a module-aware function registry key.
Expand All @@ -65,6 +69,7 @@ def function_key(module_key: ModuleKey, name: str) -> tuple[ModuleKey, str]:


@public
@typechecked
def struct_key(module_key: ModuleKey, name: str) -> tuple[ModuleKey, str]:
"""
title: Return a module-aware struct registry key.
Expand All @@ -80,6 +85,7 @@ def struct_key(module_key: ModuleKey, name: str) -> tuple[ModuleKey, str]:


@public
@typechecked
def qualified_function_name(module_key: ModuleKey, name: str) -> str:
"""
title: Return a qualified semantic function name.
Expand All @@ -95,6 +101,7 @@ def qualified_function_name(module_key: ModuleKey, name: str) -> str:


@public
@typechecked
def qualified_struct_name(module_key: ModuleKey, name: str) -> str:
"""
title: Return a qualified semantic struct name.
Expand All @@ -110,6 +117,7 @@ def qualified_struct_name(module_key: ModuleKey, name: str) -> str:


@public
@typechecked
def qualified_local_name(
module_key: ModuleKey,
kind: str,
Expand All @@ -134,6 +142,7 @@ def qualified_local_name(


@public
@typechecked
def mangle_function_name(module_key: ModuleKey, function_name: str) -> str:
"""
title: Return a deterministic LLVM function name.
Expand All @@ -149,6 +158,7 @@ def mangle_function_name(module_key: ModuleKey, function_name: str) -> str:


@public
@typechecked
def mangle_struct_name(module_key: ModuleKey, struct_name: str) -> str:
"""
title: Return a deterministic LLVM struct name.
Expand Down
18 changes: 16 additions & 2 deletions src/irx/analysis/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@

from irx import astx
from irx.analysis.resolved_nodes import ResolvedOperator, SemanticFlags
from irx.analysis.types import is_unsigned_type
from irx.analysis.types import (
common_numeric_type,
is_integer_type,
is_unsigned_type,
)
from irx.typecheck import typechecked


@typechecked
def normalize_flags(
node: astx.AST,
*,
Expand All @@ -31,10 +37,17 @@ def normalize_flags(
type: SemanticFlags
"""
explicit_unsigned = getattr(node, "unsigned", None)
promoted_type = common_numeric_type(lhs_type, rhs_type)
unsigned = (
bool(explicit_unsigned)
if explicit_unsigned is not None
else is_unsigned_type(lhs_type) or is_unsigned_type(rhs_type)
else (
is_integer_type(promoted_type) and is_unsigned_type(promoted_type)
)
or (
promoted_type is None
and (is_unsigned_type(lhs_type) or is_unsigned_type(rhs_type))
)
)
return SemanticFlags(
unsigned=unsigned,
Expand All @@ -44,6 +57,7 @@ def normalize_flags(
)


@typechecked
def normalize_operator(
op_code: str,
*,
Expand Down
1 change: 1 addition & 0 deletions src/irx/analysis/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from irx.typecheck import typechecked


@typechecked
def _module_import_specifier(node: astx.ImportFromStmt) -> str:
"""
title: Return the resolver-facing module specifier for import-from nodes.
Expand Down
2 changes: 2 additions & 0 deletions src/irx/analysis/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
from irx.analysis.module_interfaces import ModuleKey
from irx.analysis.module_symbols import qualified_local_name
from irx.analysis.resolved_nodes import SemanticSymbol
from irx.typecheck import typechecked


@public
@typechecked
def variable_symbol(
symbol_id: str,
module_key: ModuleKey,
Expand Down
Loading
Loading