From b8e73f11236542029d0a2fa9eb319019d2a4aa1b Mon Sep 17 00:00:00 2001 From: Nicolai Stawinoga Date: Tue, 21 Apr 2026 11:38:29 +0200 Subject: [PATCH 1/5] irdl: add core structural directives for attr declarative assembly format Add the foundational classes for a declarative assembly format system for ParametrizedAttribute types, mirroring the op-side system: the AttrFormatDirective ABC, the structural directives (whitespace, punctuation, keyword), AttrFormatProgram, and AttrFormatParser. Parameter and other directives are added in follow-up PRs. --- .../test_attr_declarative_assembly_format.py | 130 ++++++++++++++ xdsl/irdl/declarative_assembly_format.py | 165 +++++++++++++++++- .../declarative_assembly_format_parser.py | 66 +++++++ 3 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 tests/irdl/test_attr_declarative_assembly_format.py diff --git a/tests/irdl/test_attr_declarative_assembly_format.py b/tests/irdl/test_attr_declarative_assembly_format.py new file mode 100644 index 0000000000..bcf75a808f --- /dev/null +++ b/tests/irdl/test_attr_declarative_assembly_format.py @@ -0,0 +1,130 @@ +"""Tests for the structural directives of the attribute/type assembly format. + +These tests exercise the whitespace, punctuation and keyword directives in +isolation (without parameters), driving ``AttrFormatProgram`` directly. +""" + +from __future__ import annotations + +from io import StringIO + +import pytest + +from xdsl.context import Context +from xdsl.ir import Attribute, ParametrizedAttribute, TypeAttribute +from xdsl.irdl import irdl_attr_definition +from xdsl.irdl.declarative_assembly_format import AttrFormatProgram +from xdsl.parser import Parser +from xdsl.printer import Printer +from xdsl.utils.exceptions import ParseError + + +@irdl_attr_definition +class EmptyType(ParametrizedAttribute, TypeAttribute): + name = "test_af.empty" + + +def _program(format_str: str) -> AttrFormatProgram: + return AttrFormatProgram.from_str(format_str, EmptyType.get_irdl_definition()) + + +def _print(format_str: str) -> str: + output = StringIO() + _program(format_str).print(Printer(stream=output), EmptyType()) + return output.getvalue() + + +def _parse(format_str: str, body: str) -> list[Attribute]: + parser = Parser(Context(allow_unregistered=True), body) + return _program(format_str).parse(parser, EmptyType.get_irdl_definition()) + + +# ============================================================================ +# Whitespace directive +# ============================================================================ + + +@pytest.mark.parametrize( + "fmt, expected", + [ + ("` `", " "), + ("``", ""), + ("`\\n`", "\n"), + ], +) +def test_print_whitespace(fmt: str, expected: str): + assert _print(fmt) == expected + + +def test_parse_whitespace_is_noop(): + assert _parse("` `", "") == [] + + +def test_error_invalid_whitespace(): + with pytest.raises(ParseError, match="unexpected whitespace in directive"): + _program("` `") + + +# ============================================================================ +# Keyword directive +# ============================================================================ + + +def test_print_keyword(): + assert _print("`hello`") == "hello" + + +def test_print_two_keywords_spaced(): + assert _print("`hello` `world`") == "hello world" + + +def test_parse_keyword(): + assert _parse("`hello`", "hello") == [] + + +# ============================================================================ +# Punctuation directive +# ============================================================================ + + +def test_print_single_punctuation(): + assert _print("`<`") == "<" + + +def test_print_punctuation_after_punctuation_spaced(): + # last_was_punctuation is True, `+` is not a closer -> space inserted + assert _print("`+` `+`") == "+ +" + + +def test_print_punctuation_after_punctuation_closer(): + # last_was_punctuation is True, `>` is a closer -> no space + assert _print("`+` `>`") == "+>" + + +def test_print_punctuation_after_keyword_spaced(): + # last_was_punctuation is False, `+` is not a bracket/comma -> space + assert _print("`kw` `+`") == "kw +" + + +def test_print_punctuation_after_keyword_comma(): + # last_was_punctuation is False, `,` needs no leading space + assert _print("`kw` `,`") == "kw," + + +def test_parse_punctuation(): + assert _parse("`+`", "+") == [] + + +# ============================================================================ +# Format-string errors +# ============================================================================ + + +def test_error_unexpected_token(): + with pytest.raises(ParseError, match="unexpected token"): + _program("$foo") + + +def test_error_not_punctuation_or_identifier(): + with pytest.raises(ParseError, match="punctuation or identifier expected"): + _program("`1`") diff --git a/xdsl/irdl/declarative_assembly_format.py b/xdsl/irdl/declarative_assembly_format.py index 72bd0781b3..4979fe2f25 100644 --- a/xdsl/irdl/declarative_assembly_format.py +++ b/xdsl/irdl/declarative_assembly_format.py @@ -37,6 +37,7 @@ IRDLOperationInvT, OpDef, OptionalDef, + ParamAttrDef, Successor, VarIRConstruct, VarOperand, @@ -44,7 +45,7 @@ is_const_classvar, verify_variadic_same_size, ) -from xdsl.parser import Parser, UnresolvedOperand +from xdsl.parser import AttrParser, Parser, UnresolvedOperand from xdsl.printer import Printer from xdsl.utils.exceptions import PyRDLError, VerifyException from xdsl.utils.hints import isa @@ -1486,3 +1487,165 @@ def set_empty(self, state: ParsingState) -> None: element.set_empty(state) for element in self.else_elements: element.set_empty(state) + + +# =========================================================================== +# Attribute/Type declarative assembly format +# =========================================================================== + + +@dataclass +class AttrParsingState: + """State during parsing of a ParametrizedAttribute using declarative format.""" + + parameters: list[Attribute | None] + attr_def: ParamAttrDef + + def __init__(self, attr_def: ParamAttrDef): + self.attr_def = attr_def + self.parameters = [None] * len(attr_def.parameters) + + +@dataclass(frozen=True) +class AttrFormatDirective(ABC): + """Base class for attribute/type format directives. + + Separate from FormatDirective because operation formats parse full ops + (operands, results, regions, etc.) using Parser and ParsingState, while + attribute/type formats only fill parameter slots of a ParametrizedAttribute + using AttrParser and AttrParsingState. + """ + + @abstractmethod + def parse(self, parser: AttrParser, state: AttrParsingState) -> bool: + """Parse the directive, returning True if input was consumed.""" + + @abstractmethod + def print( + self, printer: Printer, state: PrintingState, attr: ParametrizedAttribute, / + ) -> None: + """Print the directive.""" + + +# =========================================================================== +# Shared print helpers for structural directives (whitespace, punctuation, +# keyword). These are used by both the op-side and attr-side directive classes. +# =========================================================================== + + +def _print_whitespace(printer: Printer, state: PrintingState, whitespace: str) -> None: + printer.print_string(whitespace) + state.last_was_punctuation = whitespace == "" + state.should_emit_space = False + + +def _print_punctuation( + printer: Printer, state: PrintingState, punctuation: PunctuationSpelling +) -> None: + emit_space = False + if state.should_emit_space: + if state.last_was_punctuation: + if punctuation not in (">", ")", "}", "]", ","): + emit_space = True + elif punctuation not in ("<", ">", "(", ")", "{", "}", "[", "]", ","): + emit_space = True + + if emit_space: + printer.print_string(" ") + + printer.print_string(punctuation) + + state.should_emit_space = punctuation not in ("<", "(", "{", "[") + state.last_was_punctuation = True + + +def _print_keyword(printer: Printer, state: PrintingState, keyword: str) -> None: + if state.should_emit_space: + printer.print_string(" ") + state.should_emit_space = True + state.last_was_punctuation = False + + printer.print_string(keyword) + + +# =========================================================================== +# Attr-side structural directives +# =========================================================================== + + +@dataclass(frozen=True) +class AttrWhitespaceDirective(AttrFormatDirective): + """Whitespace directive for attribute/type assembly format.""" + + whitespace: Literal[" ", "\n", ""] + + def parse(self, parser: AttrParser, state: AttrParsingState) -> bool: + return False + + def print( + self, printer: Printer, state: PrintingState, attr: ParametrizedAttribute, / + ) -> None: + _print_whitespace(printer, state, self.whitespace) + + +@dataclass(frozen=True) +class AttrPunctuationDirective(AttrFormatDirective): + """Punctuation directive for attribute/type assembly format.""" + + punctuation: PunctuationSpelling + + def parse(self, parser: AttrParser, state: AttrParsingState) -> bool: + return parser.parse_optional_punctuation(self.punctuation) is not None + + def print( + self, printer: Printer, state: PrintingState, attr: ParametrizedAttribute, / + ) -> None: + _print_punctuation(printer, state, self.punctuation) + + +@dataclass(frozen=True) +class AttrKeywordDirective(AttrFormatDirective): + """Keyword directive for attribute/type assembly format.""" + + keyword: str + + def parse(self, parser: AttrParser, state: AttrParsingState) -> bool: + return parser.parse_optional_keyword(self.keyword) is not None + + def print( + self, printer: Printer, state: PrintingState, attr: ParametrizedAttribute, / + ) -> None: + _print_keyword(printer, state, self.keyword) + + +@dataclass(frozen=True) +class AttrFormatProgram: + """ + The toplevel data structure of a declarative assembly format program + for attributes/types. + """ + + stmts: tuple[AttrFormatDirective, ...] + """The statements composing the program. They are executed in order.""" + + @staticmethod + def from_str(format_str: str, attr_def: ParamAttrDef) -> AttrFormatProgram: + from xdsl.irdl.declarative_assembly_format_parser import AttrFormatParser + + return AttrFormatParser(format_str, attr_def).parse_format() + + def parse(self, parser: AttrParser, attr_def: ParamAttrDef) -> list[Attribute]: + """Run each directive in order, returning the parsed parameter values. + + IRDL constraint verification runs when the attribute is constructed, + not during this parse step. + """ + state = AttrParsingState(attr_def) + for stmt in self.stmts: + stmt.parse(parser, state) + return cast(list[Attribute], state.parameters) + + def print(self, printer: Printer, attr: ParametrizedAttribute) -> None: + state = PrintingState(last_was_punctuation=True, should_emit_space=False) + for stmt in self.stmts: + stmt.print(printer, state, attr) diff --git a/xdsl/irdl/declarative_assembly_format_parser.py b/xdsl/irdl/declarative_assembly_format_parser.py index b67b1a26b1..6d9dc582a2 100644 --- a/xdsl/irdl/declarative_assembly_format_parser.py +++ b/xdsl/irdl/declarative_assembly_format_parser.py @@ -32,6 +32,7 @@ OptSingleBlockRegionDef, OptSuccessorDef, ParamAttrConstraint, + ParamAttrDef, ParsePropInAttrDict, SameVariadicOperandSize, SameVariadicResultSize, @@ -45,7 +46,12 @@ ) from xdsl.irdl.declarative_assembly_format import ( AttrDictDirective, + AttrFormatDirective, + AttrFormatProgram, AttributeVariable, + AttrKeywordDirective, + AttrPunctuationDirective, + AttrWhitespaceDirective, DenseArrayAttributeVariable, Directive, FormatDirective, @@ -900,3 +906,63 @@ def create_results_directive(self, inside_ref: bool) -> ResultsDirective: if not inside_ref: self.seen_result_types = [True] * len(self.seen_result_types) return ResultsDirective() + + +@dataclass(init=False) +class AttrFormatParser(BaseParser): + """Parser for attribute/type declarative assembly format strings.""" + + attr_def: ParamAttrDef + + def __init__(self, input_str: str, attr_def: ParamAttrDef): + super().__init__(ParserState(FormatLexer(Input(input_str, "")))) + self.attr_def = attr_def + + def parse_format(self) -> AttrFormatProgram: + elements: list[AttrFormatDirective] = [] + while self._current_token.kind != MLIRTokenKind.EOF: + elements.append(self.parse_format_directive()) + return AttrFormatProgram(tuple(elements)) + + def parse_format_directive(self) -> AttrFormatDirective: + if self._current_token.text == "`": + return self.parse_keyword_or_punctuation() + self.raise_error(f"unexpected token '{self._current_token.text}'") + + def parse_keyword_or_punctuation(self) -> AttrFormatDirective: + start_token = self._current_token + self.parse_characters("`") + + # \n case + if self.parse_optional_keyword("\\"): + self.parse_keyword("n") + self.parse_characters("`") + return AttrWhitespaceDirective("\n") + + # Space or empty backtick case + end_token = self._current_token + if self.parse_optional_characters("`"): + whitespace = self.lexer.input.content[ + start_token.span.end : end_token.span.start + ] + if whitespace != " " and whitespace != "": + self.raise_error( + "unexpected whitespace in directive, " + "only ` ` or `` whitespace is allowed" + ) + return AttrWhitespaceDirective(whitespace) + + # Punctuation case + if self._current_token.kind.is_punctuation(): + punctuation = self._consume_token().text + self.parse_characters("`") + assert MLIRTokenKind.is_spelling_of_punctuation(punctuation) + return AttrPunctuationDirective(punctuation) + + # Identifier case + ident = self.parse_optional_identifier() + if ident is None or ident == "`": + self.raise_error("punctuation or identifier expected") + + self.parse_characters("`") + return AttrKeywordDirective(ident) From 419b3a3d1a1bcd12aebc5423b62ad998da444de3 Mon Sep 17 00:00:00 2001 From: Nicolai Stawinoga Date: Tue, 7 Jul 2026 18:55:03 +0200 Subject: [PATCH 2/5] irdl: share structural print helpers between op and attr directives The op-side WhitespaceDirective, PunctuationDirective and KeywordDirective now delegate their print logic to the shared _print_whitespace / _print_punctuation / _print_keyword helpers instead of duplicating it, matching how the attr-side directives already use them. --- xdsl/irdl/declarative_assembly_format.py | 109 +++++++++-------------- 1 file changed, 44 insertions(+), 65 deletions(-) diff --git a/xdsl/irdl/declarative_assembly_format.py b/xdsl/irdl/declarative_assembly_format.py index 4979fe2f25..7ba41ba15e 100644 --- a/xdsl/irdl/declarative_assembly_format.py +++ b/xdsl/irdl/declarative_assembly_format.py @@ -1359,6 +1359,47 @@ def is_optional_like(self) -> bool: return False +# =========================================================================== +# Shared print helpers for structural directives (whitespace, punctuation, +# keyword). These are used by both the op-side and attr-side directive classes. +# =========================================================================== + + +def _print_whitespace(printer: Printer, state: PrintingState, whitespace: str) -> None: + printer.print_string(whitespace) + state.last_was_punctuation = whitespace == "" + state.should_emit_space = False + + +def _print_punctuation( + printer: Printer, state: PrintingState, punctuation: PunctuationSpelling +) -> None: + emit_space = False + if state.should_emit_space: + if state.last_was_punctuation: + if punctuation not in (">", ")", "}", "]", ","): + emit_space = True + elif punctuation not in ("<", ">", "(", ")", "{", "}", "[", "]", ","): + emit_space = True + + if emit_space: + printer.print_string(" ") + + printer.print_string(punctuation) + + state.should_emit_space = punctuation not in ("<", "(", "{", "[") + state.last_was_punctuation = True + + +def _print_keyword(printer: Printer, state: PrintingState, keyword: str) -> None: + if state.should_emit_space: + printer.print_string(" ") + state.should_emit_space = True + state.last_was_punctuation = False + + printer.print_string(keyword) + + @dataclass(frozen=True) class WhitespaceDirective(FormatDirective): """ @@ -1376,9 +1417,7 @@ def parse(self, parser: Parser, state: ParsingState) -> bool: return False def print(self, printer: Printer, state: PrintingState, op: IRDLOperation) -> None: - printer.print_string(self.whitespace) - state.last_was_punctuation = self.whitespace == "" - state.should_emit_space = False + _print_whitespace(printer, state, self.whitespace) @dataclass(frozen=True) @@ -1400,21 +1439,7 @@ def parse(self, parser: Parser, state: ParsingState) -> bool: return parser.parse_optional_punctuation(self.punctuation) is not None def print(self, printer: Printer, state: PrintingState, op: IRDLOperation) -> None: - emit_space = False - if state.should_emit_space: - if state.last_was_punctuation: - if self.punctuation not in (">", ")", "}", "]", ","): - emit_space = True - elif self.punctuation not in ("<", ">", "(", ")", "{", "}", "[", "]", ","): - emit_space = True - - if emit_space: - printer.print_string(" ") - - printer.print_string(self.punctuation) - - state.should_emit_space = self.punctuation not in ("<", "(", "{", "[") - state.last_was_punctuation = True + _print_punctuation(printer, state, self.punctuation) def is_optional_like(self) -> bool: return True @@ -1436,12 +1461,7 @@ def parse(self, parser: Parser, state: ParsingState) -> bool: return parser.parse_optional_keyword(self.keyword) is not None def print(self, printer: Printer, state: PrintingState, op: IRDLOperation) -> None: - if state.should_emit_space: - printer.print_string(" ") - state.should_emit_space = True - state.last_was_punctuation = False - - printer.print_string(self.keyword) + _print_keyword(printer, state, self.keyword) def is_optional_like(self) -> bool: return True @@ -1527,47 +1547,6 @@ def print( """Print the directive.""" -# =========================================================================== -# Shared print helpers for structural directives (whitespace, punctuation, -# keyword). These are used by both the op-side and attr-side directive classes. -# =========================================================================== - - -def _print_whitespace(printer: Printer, state: PrintingState, whitespace: str) -> None: - printer.print_string(whitespace) - state.last_was_punctuation = whitespace == "" - state.should_emit_space = False - - -def _print_punctuation( - printer: Printer, state: PrintingState, punctuation: PunctuationSpelling -) -> None: - emit_space = False - if state.should_emit_space: - if state.last_was_punctuation: - if punctuation not in (">", ")", "}", "]", ","): - emit_space = True - elif punctuation not in ("<", ">", "(", ")", "{", "}", "[", "]", ","): - emit_space = True - - if emit_space: - printer.print_string(" ") - - printer.print_string(punctuation) - - state.should_emit_space = punctuation not in ("<", "(", "{", "[") - state.last_was_punctuation = True - - -def _print_keyword(printer: Printer, state: PrintingState, keyword: str) -> None: - if state.should_emit_space: - printer.print_string(" ") - state.should_emit_space = True - state.last_was_punctuation = False - - printer.print_string(keyword) - - # =========================================================================== # Attr-side structural directives # =========================================================================== From 46470560787b330c3fbe78feeba30bb0e4eaa893 Mon Sep 17 00:00:00 2001 From: Nicolai Stawinoga Date: Wed, 8 Jul 2026 10:47:22 +0200 Subject: [PATCH 3/5] tests: cover empty assembly format string An empty format string is a valid program (no directives): it prints nothing and parses as a no-op. Addresses review feedback on #5900. --- tests/irdl/test_attr_declarative_assembly_format.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/irdl/test_attr_declarative_assembly_format.py b/tests/irdl/test_attr_declarative_assembly_format.py index bcf75a808f..fdc9460b0a 100644 --- a/tests/irdl/test_attr_declarative_assembly_format.py +++ b/tests/irdl/test_attr_declarative_assembly_format.py @@ -39,6 +39,18 @@ def _parse(format_str: str, body: str) -> list[Attribute]: return _program(format_str).parse(parser, EmptyType.get_irdl_definition()) +# ============================================================================ +# Empty format +# ============================================================================ + + +def test_empty_format_is_valid(): + program = _program("") + assert program.stmts == () + assert _print("") == "" + assert _parse("", "") == [] + + # ============================================================================ # Whitespace directive # ============================================================================ From c28dec975fec484fc03c4a677e7de6340d48807c Mon Sep 17 00:00:00 2001 From: Nicolai Stawinoga Date: Wed, 8 Jul 2026 11:25:47 +0200 Subject: [PATCH 4/5] irdl: reduce attr assembly format PR to minimal plumbing Per review, shrink this PR to only what is needed for an empty assembly_format to work: drop the whitespace, punctuation and keyword directives, and revert the shared print helpers (restoring the op-side directives to their original inline logic). Concrete directives will follow in later PRs. --- .../test_attr_declarative_assembly_format.py | 97 +------------- xdsl/irdl/declarative_assembly_format.py | 118 ++++-------------- .../declarative_assembly_format_parser.py | 43 ------- 3 files changed, 27 insertions(+), 231 deletions(-) diff --git a/tests/irdl/test_attr_declarative_assembly_format.py b/tests/irdl/test_attr_declarative_assembly_format.py index fdc9460b0a..b2d3f84ed6 100644 --- a/tests/irdl/test_attr_declarative_assembly_format.py +++ b/tests/irdl/test_attr_declarative_assembly_format.py @@ -1,7 +1,7 @@ -"""Tests for the structural directives of the attribute/type assembly format. +"""Tests for the attribute/type declarative assembly format plumbing. -These tests exercise the whitespace, punctuation and keyword directives in -isolation (without parameters), driving ``AttrFormatProgram`` directly. +These tests drive ``AttrFormatProgram`` directly. At this stage only the empty +format is supported; concrete directives are added in later commits. """ from __future__ import annotations @@ -39,11 +39,6 @@ def _parse(format_str: str, body: str) -> list[Attribute]: return _program(format_str).parse(parser, EmptyType.get_irdl_definition()) -# ============================================================================ -# Empty format -# ============================================================================ - - def test_empty_format_is_valid(): program = _program("") assert program.stmts == () @@ -51,92 +46,6 @@ def test_empty_format_is_valid(): assert _parse("", "") == [] -# ============================================================================ -# Whitespace directive -# ============================================================================ - - -@pytest.mark.parametrize( - "fmt, expected", - [ - ("` `", " "), - ("``", ""), - ("`\\n`", "\n"), - ], -) -def test_print_whitespace(fmt: str, expected: str): - assert _print(fmt) == expected - - -def test_parse_whitespace_is_noop(): - assert _parse("` `", "") == [] - - -def test_error_invalid_whitespace(): - with pytest.raises(ParseError, match="unexpected whitespace in directive"): - _program("` `") - - -# ============================================================================ -# Keyword directive -# ============================================================================ - - -def test_print_keyword(): - assert _print("`hello`") == "hello" - - -def test_print_two_keywords_spaced(): - assert _print("`hello` `world`") == "hello world" - - -def test_parse_keyword(): - assert _parse("`hello`", "hello") == [] - - -# ============================================================================ -# Punctuation directive -# ============================================================================ - - -def test_print_single_punctuation(): - assert _print("`<`") == "<" - - -def test_print_punctuation_after_punctuation_spaced(): - # last_was_punctuation is True, `+` is not a closer -> space inserted - assert _print("`+` `+`") == "+ +" - - -def test_print_punctuation_after_punctuation_closer(): - # last_was_punctuation is True, `>` is a closer -> no space - assert _print("`+` `>`") == "+>" - - -def test_print_punctuation_after_keyword_spaced(): - # last_was_punctuation is False, `+` is not a bracket/comma -> space - assert _print("`kw` `+`") == "kw +" - - -def test_print_punctuation_after_keyword_comma(): - # last_was_punctuation is False, `,` needs no leading space - assert _print("`kw` `,`") == "kw," - - -def test_parse_punctuation(): - assert _parse("`+`", "+") == [] - - -# ============================================================================ -# Format-string errors -# ============================================================================ - - def test_error_unexpected_token(): with pytest.raises(ParseError, match="unexpected token"): _program("$foo") - - -def test_error_not_punctuation_or_identifier(): - with pytest.raises(ParseError, match="punctuation or identifier expected"): - _program("`1`") diff --git a/xdsl/irdl/declarative_assembly_format.py b/xdsl/irdl/declarative_assembly_format.py index 7ba41ba15e..ea21907f6a 100644 --- a/xdsl/irdl/declarative_assembly_format.py +++ b/xdsl/irdl/declarative_assembly_format.py @@ -1359,47 +1359,6 @@ def is_optional_like(self) -> bool: return False -# =========================================================================== -# Shared print helpers for structural directives (whitespace, punctuation, -# keyword). These are used by both the op-side and attr-side directive classes. -# =========================================================================== - - -def _print_whitespace(printer: Printer, state: PrintingState, whitespace: str) -> None: - printer.print_string(whitespace) - state.last_was_punctuation = whitespace == "" - state.should_emit_space = False - - -def _print_punctuation( - printer: Printer, state: PrintingState, punctuation: PunctuationSpelling -) -> None: - emit_space = False - if state.should_emit_space: - if state.last_was_punctuation: - if punctuation not in (">", ")", "}", "]", ","): - emit_space = True - elif punctuation not in ("<", ">", "(", ")", "{", "}", "[", "]", ","): - emit_space = True - - if emit_space: - printer.print_string(" ") - - printer.print_string(punctuation) - - state.should_emit_space = punctuation not in ("<", "(", "{", "[") - state.last_was_punctuation = True - - -def _print_keyword(printer: Printer, state: PrintingState, keyword: str) -> None: - if state.should_emit_space: - printer.print_string(" ") - state.should_emit_space = True - state.last_was_punctuation = False - - printer.print_string(keyword) - - @dataclass(frozen=True) class WhitespaceDirective(FormatDirective): """ @@ -1417,7 +1376,9 @@ def parse(self, parser: Parser, state: ParsingState) -> bool: return False def print(self, printer: Printer, state: PrintingState, op: IRDLOperation) -> None: - _print_whitespace(printer, state, self.whitespace) + printer.print_string(self.whitespace) + state.last_was_punctuation = self.whitespace == "" + state.should_emit_space = False @dataclass(frozen=True) @@ -1439,7 +1400,21 @@ def parse(self, parser: Parser, state: ParsingState) -> bool: return parser.parse_optional_punctuation(self.punctuation) is not None def print(self, printer: Printer, state: PrintingState, op: IRDLOperation) -> None: - _print_punctuation(printer, state, self.punctuation) + emit_space = False + if state.should_emit_space: + if state.last_was_punctuation: + if self.punctuation not in (">", ")", "}", "]", ","): + emit_space = True + elif self.punctuation not in ("<", ">", "(", ")", "{", "}", "[", "]", ","): + emit_space = True + + if emit_space: + printer.print_string(" ") + + printer.print_string(self.punctuation) + + state.should_emit_space = self.punctuation not in ("<", "(", "{", "[") + state.last_was_punctuation = True def is_optional_like(self) -> bool: return True @@ -1461,7 +1436,12 @@ def parse(self, parser: Parser, state: ParsingState) -> bool: return parser.parse_optional_keyword(self.keyword) is not None def print(self, printer: Printer, state: PrintingState, op: IRDLOperation) -> None: - _print_keyword(printer, state, self.keyword) + if state.should_emit_space: + printer.print_string(" ") + state.should_emit_space = True + state.last_was_punctuation = False + + printer.print_string(self.keyword) def is_optional_like(self) -> bool: return True @@ -1547,56 +1527,6 @@ def print( """Print the directive.""" -# =========================================================================== -# Attr-side structural directives -# =========================================================================== - - -@dataclass(frozen=True) -class AttrWhitespaceDirective(AttrFormatDirective): - """Whitespace directive for attribute/type assembly format.""" - - whitespace: Literal[" ", "\n", ""] - - def parse(self, parser: AttrParser, state: AttrParsingState) -> bool: - return False - - def print( - self, printer: Printer, state: PrintingState, attr: ParametrizedAttribute, / - ) -> None: - _print_whitespace(printer, state, self.whitespace) - - -@dataclass(frozen=True) -class AttrPunctuationDirective(AttrFormatDirective): - """Punctuation directive for attribute/type assembly format.""" - - punctuation: PunctuationSpelling - - def parse(self, parser: AttrParser, state: AttrParsingState) -> bool: - return parser.parse_optional_punctuation(self.punctuation) is not None - - def print( - self, printer: Printer, state: PrintingState, attr: ParametrizedAttribute, / - ) -> None: - _print_punctuation(printer, state, self.punctuation) - - -@dataclass(frozen=True) -class AttrKeywordDirective(AttrFormatDirective): - """Keyword directive for attribute/type assembly format.""" - - keyword: str - - def parse(self, parser: AttrParser, state: AttrParsingState) -> bool: - return parser.parse_optional_keyword(self.keyword) is not None - - def print( - self, printer: Printer, state: PrintingState, attr: ParametrizedAttribute, / - ) -> None: - _print_keyword(printer, state, self.keyword) - - @dataclass(frozen=True) class AttrFormatProgram: """ diff --git a/xdsl/irdl/declarative_assembly_format_parser.py b/xdsl/irdl/declarative_assembly_format_parser.py index 6d9dc582a2..71576e82ab 100644 --- a/xdsl/irdl/declarative_assembly_format_parser.py +++ b/xdsl/irdl/declarative_assembly_format_parser.py @@ -49,9 +49,6 @@ AttrFormatDirective, AttrFormatProgram, AttributeVariable, - AttrKeywordDirective, - AttrPunctuationDirective, - AttrWhitespaceDirective, DenseArrayAttributeVariable, Directive, FormatDirective, @@ -925,44 +922,4 @@ def parse_format(self) -> AttrFormatProgram: return AttrFormatProgram(tuple(elements)) def parse_format_directive(self) -> AttrFormatDirective: - if self._current_token.text == "`": - return self.parse_keyword_or_punctuation() self.raise_error(f"unexpected token '{self._current_token.text}'") - - def parse_keyword_or_punctuation(self) -> AttrFormatDirective: - start_token = self._current_token - self.parse_characters("`") - - # \n case - if self.parse_optional_keyword("\\"): - self.parse_keyword("n") - self.parse_characters("`") - return AttrWhitespaceDirective("\n") - - # Space or empty backtick case - end_token = self._current_token - if self.parse_optional_characters("`"): - whitespace = self.lexer.input.content[ - start_token.span.end : end_token.span.start - ] - if whitespace != " " and whitespace != "": - self.raise_error( - "unexpected whitespace in directive, " - "only ` ` or `` whitespace is allowed" - ) - return AttrWhitespaceDirective(whitespace) - - # Punctuation case - if self._current_token.kind.is_punctuation(): - punctuation = self._consume_token().text - self.parse_characters("`") - assert MLIRTokenKind.is_spelling_of_punctuation(punctuation) - return AttrPunctuationDirective(punctuation) - - # Identifier case - ident = self.parse_optional_identifier() - if ident is None or ident == "`": - self.raise_error("punctuation or identifier expected") - - self.parse_characters("`") - return AttrKeywordDirective(ident) From 9e157d11550bc4680248b5ad03e008f916559250 Mon Sep 17 00:00:00 2001 From: Nicolai Stawinoga Date: Wed, 8 Jul 2026 14:56:33 +0200 Subject: [PATCH 5/5] irdl: address review on attr assembly format plumbing - Assert the format is empty in AttrFormatProgram.parse/print instead of iterating over stmts, removing the two uncovered loop bodies (the loop returns in a later PR alongside concrete directives). - Reformat the new docstrings to start on their own line. - Split the empty-format test into program/print-parse cases and add an end-to-end parse of !test_af.empty. --- .../test_attr_declarative_assembly_format.py | 14 ++++++-- xdsl/irdl/declarative_assembly_format.py | 34 ++++++++++++------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/tests/irdl/test_attr_declarative_assembly_format.py b/tests/irdl/test_attr_declarative_assembly_format.py index b2d3f84ed6..df548df5bd 100644 --- a/tests/irdl/test_attr_declarative_assembly_format.py +++ b/tests/irdl/test_attr_declarative_assembly_format.py @@ -39,13 +39,21 @@ def _parse(format_str: str, body: str) -> list[Attribute]: return _program(format_str).parse(parser, EmptyType.get_irdl_definition()) -def test_empty_format_is_valid(): - program = _program("") - assert program.stmts == () +def test_empty_format_produces_empty_program(): + assert _program("").stmts == () + + +def test_empty_format_prints_and_parses_nothing(): assert _print("") == "" assert _parse("", "") == [] +def test_parse_attribute_end_to_end(): + ctx = Context() + ctx.load_attr_or_type(EmptyType) + assert Parser(ctx, "!test_af.empty").parse_type() == EmptyType() + + def test_error_unexpected_token(): with pytest.raises(ParseError, match="unexpected token"): _program("$foo") diff --git a/xdsl/irdl/declarative_assembly_format.py b/xdsl/irdl/declarative_assembly_format.py index ea21907f6a..e45cc00917 100644 --- a/xdsl/irdl/declarative_assembly_format.py +++ b/xdsl/irdl/declarative_assembly_format.py @@ -1496,7 +1496,9 @@ def set_empty(self, state: ParsingState) -> None: @dataclass class AttrParsingState: - """State during parsing of a ParametrizedAttribute using declarative format.""" + """ + State during parsing of a ParametrizedAttribute using declarative format. + """ parameters: list[Attribute | None] attr_def: ParamAttrDef @@ -1508,7 +1510,8 @@ def __init__(self, attr_def: ParamAttrDef): @dataclass(frozen=True) class AttrFormatDirective(ABC): - """Base class for attribute/type format directives. + """ + Base class for attribute/type format directives. Separate from FormatDirective because operation formats parse full ops (operands, results, regions, etc.) using Parser and ParsingState, while @@ -1518,13 +1521,17 @@ class AttrFormatDirective(ABC): @abstractmethod def parse(self, parser: AttrParser, state: AttrParsingState) -> bool: - """Parse the directive, returning True if input was consumed.""" + """ + Parse the directive, returning True if input was consumed. + """ @abstractmethod def print( self, printer: Printer, state: PrintingState, attr: ParametrizedAttribute, / ) -> None: - """Print the directive.""" + """ + Print the directive. + """ @dataclass(frozen=True) @@ -1535,7 +1542,9 @@ class AttrFormatProgram: """ stmts: tuple[AttrFormatDirective, ...] - """The statements composing the program. They are executed in order.""" + """ + The statements composing the program. They are executed in order. + """ @staticmethod def from_str(format_str: str, attr_def: ParamAttrDef) -> AttrFormatProgram: @@ -1544,17 +1553,16 @@ def from_str(format_str: str, attr_def: ParamAttrDef) -> AttrFormatProgram: return AttrFormatParser(format_str, attr_def).parse_format() def parse(self, parser: AttrParser, attr_def: ParamAttrDef) -> list[Attribute]: - """Run each directive in order, returning the parsed parameter values. + """ + Return the parsed parameter values. - IRDL constraint verification runs when the attribute is constructed, - not during this parse step. + Only the empty format is supported at this stage, so there are no + directives to run yet; IRDL constraint verification runs when the + attribute is constructed, not during this parse step. """ + assert not self.stmts state = AttrParsingState(attr_def) - for stmt in self.stmts: - stmt.parse(parser, state) return cast(list[Attribute], state.parameters) def print(self, printer: Printer, attr: ParametrizedAttribute) -> None: - state = PrintingState(last_was_punctuation=True, should_emit_space=False) - for stmt in self.stmts: - stmt.print(printer, state, attr) + assert not self.stmts