From 136aa681130eda1f76fe20930eea62ab451698c0 Mon Sep 17 00:00:00 2001 From: Andi Hellmund Date: Tue, 10 Mar 2026 23:26:53 +0100 Subject: [PATCH] new languge syntax --- CLAUDE.md | 80 ++------------- README.md | 69 ++++++++----- docs/LANGUAGE_SYNTAX.md | 220 +++++++++++++++++++++++++++++++++------- 3 files changed, 237 insertions(+), 132 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ffc280..70ea607 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,9 +2,9 @@ ## Project Overview -ArchML is a text-based DSL for defining software architecture alongside code. It covers functional, behavioral, and deployment architecture domains with consistency checking, navigable web views, and native Sphinx integration. Architecture files use the `.archml` extension. +ArchML is a text-based DSL for defining software architecture alongside code. +It covers functional, behavioral, and deployment architecture domains with consistency checking, navigable web views, and native Sphinx integration. Architecture files use the `.archml` extension. -The project is in early development. The DSL syntax for functional architecture is specified in `docs/LANGUAGE_SYNTAX.md`. The overall vision and landscape analysis are in `docs/PROJECT_SCOPE.md`. ## Tech Stack @@ -13,42 +13,11 @@ The project is in early development. The DSL syntax for functional architecture - **Linter/formatter**: ruff - **Type checker**: ty - **Testing**: pytest -- **Documentation**: Sphinx (ArchML views will embed natively via a Sphinx extension) +- **Documentation**: Sphinx - **Distribution**: PyPI -## Project Structure (Target) -``` -archml/ -├── CLAUDE.md -├── README.md -├── LICENSE -├── pyproject.toml -├── docs/ -│ ├── PROJECT_SCOPE.md -│ ├── LANGUAGE_SYNTAX.md -│ └── sphinx/ # Sphinx documentation source -├── src/ -│ └── archml/ -│ ├── __init__.py -│ ├── parser/ # Lexer and parser for .archml files -│ ├── model/ # Semantic model (systems, components, interfaces, etc.) -│ ├── validation/ # Consistency checks (dangling refs, unused interfaces) -│ ├── views/ # View generation and rendering -│ ├── sphinx_ext/ # Sphinx extension for embedding architecture views -│ ├── lsp/ # Language server (LSP) for VS Code integration -│ ├── webui/ # Dash-based web UI for interactive architecture viewing -│ └── cli/ # Command-line interface -└── tests/ # All tests (mirrors src/ structure) - ├── parser/ - ├── model/ - ├── validation/ - ├── views/ - ├── sphinx_ext/ - ├── lsp/ - ├── webui/ - └── cli/ -``` +## Project Structure (Target) ## Common Commands @@ -59,9 +28,6 @@ uv sync # Run all tests uv run pytest -# Run tests with coverage -uv run pytest --cov=archml - # Run a specific test file or test uv run pytest tests/parser/test_lexer.py uv run pytest -k "test_parse_component" @@ -72,12 +38,6 @@ uv run ruff format src/ tests/ # Type check uv run ty check src/ - -# Build the package -uv build - -# Build Sphinx docs -uv run sphinx-build docs/sphinx docs/sphinx/_build ``` ## Development Methodology @@ -90,37 +50,15 @@ Every new feature requires thorough testing before it is considered complete. Th 4. Ensure all tests pass, ruff reports no issues, and ty finds no type errors using `uv run tools/ci.py`. 5. Commit with a clear message describing the change. -Tests are not optional. A feature without tests is not done. - -## ArchML Language Quick Reference +Tests are not optional. +A feature without tests is not done. -The DSL defines architecture through these core constructs: -- **`system`** — groups components or sub-systems -- **`component`** — module with `requires` and `provides` interface declarations; supports nesting -- **`user`** — human actor (role or persona) that interacts with the system; a leaf node -- **`interface`** — typed contract between elements; supports versioning (`@v1`, `@v2`) -- **`type`** — reusable data structure used within interfaces -- **`enum`** — constrained set of named values -- **`connect`** — data-flow edge linking a required interface to a provided interface (`connect A -> B by Interface`) -- **`external`** — marks a system, component, or user as outside the development boundary -- **`import` / `use`** — multi-file composition; `use` always includes the entity type (e.g., `use component X`) -- **`tags`** — arbitrary labels for filtering and view generation -- **`field`** — typed data element with optional `description` and `schema` annotations - -Primitive types: `String`, `Int`, `Float`, `Decimal`, `Bool`, `Bytes`, `Timestamp`, `Datetime` -Container types: `List`, `Map`, `Optional` -Filesystem types: `File` (with `filetype`, `schema`), `Directory` (with `schema`) +## ArchML Language Quick Reference Full syntax specification: `docs/LANGUAGE_SYNTAX.md` -## Architecture and Design Decisions -- The parser produces an AST which is then lowered into a semantic model. Validation runs on the semantic model, not the AST. -- Views are not part of the architecture language. They will be defined in a separate view DSL that references model entities. -- The Sphinx extension reads `.archml` files directly and renders views inline — it is not an export pipeline. -- The CLI is the primary user entry point for parsing, validating, and generating views outside of Sphinx. -- A Language Server Protocol (LSP) implementation provides IDE support (diagnostics, completion, go-to-definition) for `.archml` files, with a VS Code extension as the primary client. ## Coding Conventions @@ -129,6 +67,7 @@ Full syntax specification: `docs/LANGUAGE_SYNTAX.md` - Prefer dataclasses or attrs for model types. - Keep modules focused: one responsibility per module. - The test directory structure mirrors the source structure. Every module in `src/archml//` has a corresponding directory in `tests//`. Test files are prefixed with `test_`: `src/archml/parser/lexer.py` -> `tests/parser/test_lexer.py`. +- Use proper docstrings for public functions. - Every Python file follows this layout: ```python @@ -142,7 +81,6 @@ import ... # ############### def public_function() -> None: - """Docstring describing the function.""" ... # ################ @@ -153,4 +91,4 @@ def _private_helper() -> None: ... ``` -The copyright header and imports come first. Public interface (classes, functions, constants) is separated from private implementation by section comments. All private members are prefixed with an underscore. +The copyright header and imports come first. Public interface (classes, functions, constants) is separated from private implementation by section comments. All private members are prefixed with an underscore. \ No newline at end of file diff --git a/README.md b/README.md index 0c8cbc8..77f1024 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,26 @@ system ECommerce { title = "Order Service" description = "Accepts, validates, and processes customer orders." - requires OrderRequest - requires PaymentRequest - requires InventoryCheck - provides OrderConfirmation + // Internal pipeline: Validator feeds into Processor + component Validator { + requires OrderRequest as input + provides ValidationResult as output + } + + component Processor { + requires ValidationResult as input + requires PaymentRequest + requires InventoryCheck + provides OrderConfirmation + } + + connect Validator.output -> Processor.input + + // Promote inner ports to the OrderService boundary + expose Validator.input // requires OrderRequest + expose Processor.PaymentRequest // requires PaymentRequest + expose Processor.InventoryCheck // requires InventoryCheck + expose Processor.OrderConfirmation // provides OrderConfirmation } component PaymentGateway { @@ -104,14 +120,15 @@ system ECommerce { component InventoryManager { title = "Inventory Manager" - requires InventoryCheck + requires InventoryCheck as requests [1..*] // accepts requests from multiple sources provides InventoryStatus } - connect Customer -> OrderService by OrderRequest - connect OrderService -> Customer by OrderConfirmation - connect OrderService -> PaymentGateway by PaymentRequest - connect OrderService -> InventoryManager by InventoryCheck { + // Short form: interface is unambiguous on both sides + connect Customer -> OrderService by OrderRequest + connect OrderService -> Customer by OrderConfirmation + connect OrderService -> PaymentGateway by PaymentRequest + connect OrderService -> InventoryManager by InventoryCheck { protocol = "HTTP" } connect PaymentGateway -> StripeAPI by PaymentRequest { @@ -125,21 +142,25 @@ Large architectures split naturally across files. A `from ... import` statement ## Language at a Glance -| Keyword | Purpose | -| ----------------------- | --------------------------------------------------------------------- | -| `system` | Group of components or sub-systems with a shared goal | -| `component` | Module with a clear responsibility; may nest sub-components | -| `user` | Human actor (role or persona) that interacts with the system | -| `interface` | Named contract of typed data fields; supports `@v1`, `@v2` versioning | -| `type` | Reusable data structure (used within interfaces) | -| `enum` | Constrained set of named values | -| `field` | Named, typed data element with optional `description` and `schema` | -| `requires` / `provides` | Declare consumed and exposed interfaces on a component or user | -| `connect A -> B by I` | Data-flow edge linking a required interface to a provided one | -| `external` | Marks a system, component, or user as outside the development boundary | -| `from … import` | Bring specific definitions from another file into scope | -| `use component X` | Place an imported entity inside a system | -| `tags` | Arbitrary labels for filtering and view generation | +| Keyword | Purpose | +| ---------------------------- | --------------------------------------------------------------------------------- | +| `system` | Group of components or sub-systems with a shared goal | +| `component` | Module with a clear responsibility; may nest sub-components | +| `user` | Human actor (role or persona) that interacts with the system | +| `interface` | Named contract of typed data fields; supports `@v1`, `@v2` versioning | +| `type` | Reusable data structure (used within interfaces) | +| `enum` | Constrained set of named values | +| `field` | Named, typed data element with optional `description` and `schema` | +| `requires` / `provides` | Declare consumed and exposed interface ports on a component or user | +| `requires X as name` | Named port — required when the same interface appears more than once | +| `requires X as name [1..*]` | Multi-port with cardinality — for fan-in patterns; `[N]`, `[*]`, `[M..N]` also valid | +| `expose Sub.port` | Promote a nested component's port to the enclosing component's boundary | +| `connect A -> B by I` | Short form: wire interface I between A and B (when I is unambiguous on both sides) | +| `connect A.p -> B.p` | Long form: wire named ports directly (no `by` needed) | +| `external` | Marks a system, component, or user as outside the development boundary | +| `from … import` | Bring specific definitions from another file into scope | +| `use component X` | Place an imported entity inside a system | +| `tags` | Arbitrary labels for filtering and view generation | Primitive types: `String`, `Int`, `Float`, `Decimal`, `Bool`, `Bytes`, `Timestamp`, `Datetime` Container types: `List`, `Map`, `Optional` diff --git a/docs/LANGUAGE_SYNTAX.md b/docs/LANGUAGE_SYNTAX.md index e4d7f0c..bd5d6e5 100644 --- a/docs/LANGUAGE_SYNTAX.md +++ b/docs/LANGUAGE_SYNTAX.md @@ -158,20 +158,25 @@ component OrderService { component Validator { title = "Order Validator" - requires OrderRequest - provides ValidationResult + requires OrderRequest as input + provides ValidationResult as output } component Processor { title = "Order Processor" - requires ValidationResult + requires ValidationResult as input requires PaymentRequest requires InventoryCheck provides OrderConfirmation } - connect Validator -> Processor by ValidationResult + connect Validator.output -> Processor.input + + expose Validator.input // OrderService requires OrderRequest + expose Processor.PaymentRequest // OrderService requires PaymentRequest + expose Processor.InventoryCheck // OrderService requires InventoryCheck + expose Processor.OrderConfirmation // OrderService provides OrderConfirmation } ``` @@ -246,21 +251,133 @@ external user Admin { External entities appear in diagrams with distinct styling. They cannot be further decomposed (they are opaque). +## Ports + +Every `requires` and `provides` declaration on a component or user defines a **port** — a named connection point for a specific interface use. Ports are the targets that `connect` statements wire together. + +### Port names + +By default a port's name is the interface name: + +``` +component OrderService { + requires PaymentRequest // port named "PaymentRequest" + provides OrderConfirmation // port named "OrderConfirmation" +} +``` + +An explicit name is assigned with the `as` keyword: + +``` +component OrderService { + requires InventoryCheck as primary_inventory + requires InventoryCheck as backup_inventory + provides OrderConfirmation +} +``` + +A port name is **required** when a component declares two or more ports for the same interface in the same direction. When the interface appears only once in a given direction, naming is optional but encouraged whenever the port has a meaningful semantic role: + +``` +component Filter { + requires DataStream as input + provides DataStream as output +} +``` + +### Port multiplicity + +A port can accept more than one connection with a multiplicity annotation. Multiplicity always requires an explicit port name: + +| Annotation | Meaning | +| ---------- | --------------------------- | +| *(none)* | exactly one connection | +| `[N]` | exactly N connections | +| `[*]` | zero or more connections | +| `[1..*]` | one or more connections | +| `[M..N]` | between M and N connections | + +``` +component Aggregator { + requires Result as workers [1..*] // accepts results from one or more workers + provides Summary +} +``` + +Multiple `connect` statements targeting the same multi-port are valid. The validator checks the actual connection count against the declared cardinality. + +### Boundary propagation with `expose` + +A nested component's port can be promoted to the enclosing component's boundary with the `expose` keyword. This makes the inner port reachable from outside without requiring callers to know about internal structure: + +``` +component OrderService { + component Validator { + requires OrderRequest as input + provides ValidationResult as output + } + + component Processor { + requires ValidationResult as input + provides OrderConfirmation + } + + connect Validator.output -> Processor.input + + expose Validator.input // OrderService now requires OrderRequest + expose Processor.OrderConfirmation // OrderService now provides OrderConfirmation +} +``` + +The exposed port retains its original name on the enclosing boundary. An alias can be assigned with `as`: + +``` +expose Validator.input as request +``` + +`expose` propagates one level at a time. To surface a port through multiple nesting levels, each intermediate component must declare its own `expose`. + +Direct `connect` from an outer scope into a nested component is not allowed. Use `expose` to make internal ports reachable at the enclosing boundary first. + ## Connections -Connections are the data-flow edges of the architecture graph. A connection always links a **required** interface byone side to a **provided** interface on the other. The arrow `->` indicates the direction of the request (who initiates); data may flow in both directions as part of request/response. +Connections are the data-flow edges of the architecture graph. A connection always links a **required** port on one side to a **provided** port on the other. The arrow `->` indicates the direction of the request (who initiates); data may flow in both directions as part of request/response. + +All connections are unidirectional. For bidirectional communication, use two separate connections. + +### Short form -All connections are unidirectional. For bidirectional communication, use two separate connections: +When each side has exactly one port for the given interface, the component names and the interface name are sufficient: ``` connect -> by +``` + +``` +connect Customer -> OrderService by OrderRequest +connect ServiceA -> ServiceB by RequestToB +connect ServiceB -> ServiceA by ResponseToA +``` + +### Long form -// Bidirectional: two explicit connections. -connect ServiceA -> ServiceB by RequestToB -connect ServiceB -> ServiceA by ResponseToA +When ports are named — because an interface appears more than once on a component, or for explicitness — reference port names directly. The interface is implied from the port and the `by` clause is omitted: + +``` +connect . -> . ``` -Connections may carry annotations. Each attribute must appear on its own line: +``` +connect OrderService.primary_inventory -> PrimaryInventory.InventoryCheck +connect OrderService.backup_inventory -> BackupInventory.InventoryCheck +connect Validator.output -> Processor.input +``` + +Both forms are equivalent when the short form is unambiguous. The arrow direction (`->`) identifies which side is the providing port (left) and which is the requiring port (right), so a component that has both `requires X` and `provides X` for the same interface is still unambiguous. + +### Annotations + +Either form can carry a block of attributes. Each attribute must appear on its own line: ``` connect OrderService -> PaymentGateway by PaymentRequest { @@ -270,6 +387,10 @@ connect OrderService -> PaymentGateway by PaymentRequest { } ``` +### Encapsulation + +`connect` statements may only reference ports that are directly visible in the current scope — that is, ports belonging to direct children of the enclosing component or system. Connecting into a grandchild or deeper is not permitted; use `expose` to propagate the inner port to the enclosing boundary first. + ## Tags Any named entity can carry **tags** for filtering and view generation: @@ -444,6 +565,11 @@ interface InventoryStatus { field available: Bool } +interface ValidationResult { + field order_id: String + field valid: Bool +} + interface ReportOutput { field report: File { filetype = "PDF" @@ -452,16 +578,34 @@ interface ReportOutput { } // file: components/order_service.archml -from types import OrderItem, OrderRequest, PaymentRequest, InventoryCheck, OrderConfirmation +from types import OrderItem, OrderRequest, ValidationResult, PaymentRequest, InventoryCheck, OrderConfirmation component OrderService { title = "Order Service" description = "Accepts, validates, and processes customer orders." - requires OrderRequest - requires PaymentRequest - requires InventoryCheck - provides OrderConfirmation + component Validator { + title = "Order Validator" + + requires OrderRequest as input + provides ValidationResult as output + } + + component Processor { + title = "Order Processor" + + requires ValidationResult as input + requires PaymentRequest + requires InventoryCheck + provides OrderConfirmation + } + + connect Validator.output -> Processor.input + + expose Validator.input // requires OrderRequest + expose Processor.PaymentRequest // requires PaymentRequest + expose Processor.InventoryCheck // requires InventoryCheck + expose Processor.OrderConfirmation // provides OrderConfirmation } // file: systems/ecommerce.archml @@ -518,28 +662,30 @@ system ECommerce { ## Summary of Keywords -| Keyword | Purpose | -| ------------- | ----------------------------------------------------------------------------------------------------- | -| `system` | Group of components or sub-systems with a shared goal. | -| `component` | Module with a clear responsibility; may nest sub-components. | -| `user` | Human actor (role or persona) that interacts with the system; a leaf node. | -| `interface` | Named contract of typed data fields. Supports versioning via `@v1`, `@v2`, etc. | -| `type` | Reusable data structure (used within interfaces). | -| `enum` | Constrained set of named values. | -| `field` | Named, typed data element. Supports `description` and `schema` annotations. | -| `filetype` | Annotation bya `File` field specifying its format. | -| `schema` | Free-text annotation describing expected content or format. | -| `requires` | Declares an interface that an element consumes (listed before `provides`). | -| `provides` | Declares an interface that an element exposes. | -| `connect` | Links a required interface to a provided interface. | -| `by` | Specifies the interface in a `connect` statement (`connect A -> B by Interface`). | -| `from` | Introduces the source path in an import statement (`from path import Name`). | -| `import` | Names the specific entities to bring into scope; always paired with `from` (`from path import Name`). | -| `use` | Places an imported entity into a system or component (e.g., `use component X`). | -| `external` | Marks a system, component, or user as outside the development boundary. | -| `tags` | Arbitrary labels for filtering and view generation. | -| `title` | Human-readable display name. | -| `description` | Longer explanation of an entity's purpose. | +| Keyword | Purpose | +| --------------- | ------------------------------------------------------------------------------------------------------------------ | +| `system` | Group of components or sub-systems with a shared goal. | +| `component` | Module with a clear responsibility; may nest sub-components. | +| `user` | Human actor (role or persona) that interacts with the system; a leaf node. | +| `interface` | Named contract of typed data fields. Supports versioning via `@v1`, `@v2`, etc. | +| `type` | Reusable data structure (used within interfaces). | +| `enum` | Constrained set of named values. | +| `field` | Named, typed data element. Supports `description` and `schema` annotations. | +| `filetype` | Annotation on a `File` field specifying its format. | +| `schema` | Free-text annotation describing expected content or format. | +| `requires` | Declares an interface port that an element consumes (listed before `provides`). | +| `provides` | Declares an interface port that an element exposes. | +| `as` | Assigns a name to a port (`requires X as name`) or provides an alias in `expose` (`expose Sub.port as name`). | +| `expose` | Promotes a nested component's port to the enclosing component's boundary (`expose Sub.port`). | +| `connect` | Wires a providing port to a requiring port. Short form: `connect A -> B by X`. Long form: `connect A.p -> B.p`. | +| `by` | Specifies the interface in the short form of `connect` (`connect A -> B by Interface`). | +| `from` | Introduces the source path in an import statement (`from path import Name`). | +| `import` | Names the specific entities to bring into scope; always paired with `from` (`from path import Name`). | +| `use` | Places an imported entity into a system or component (e.g., `use component X`). | +| `external` | Marks a system, component, or user as outside the development boundary. | +| `tags` | Arbitrary labels for filtering and view generation. | +| `title` | Human-readable display name. | +| `description` | Longer explanation of an entity's purpose. | ## Scope Boundaries