Skip to content

Commit d68b685

Browse files
Backend formatter interface (#806)
* Dispatch in formatters * Lock down interface on formatter for single capability * Dispatch on __call__ * More * Missed one * implementation.py -> formatter.py * Add ABC as interface of Formatter * Fix import * one more * Apply suggestions from code review Co-authored-by: Jørgen Schartum Dokken <dokken@simula.no> * Fixup syntax * StatementList as in C formatter --------- Co-authored-by: Jørgen Schartum Dokken <dokken@simula.no>
1 parent 842b66d commit d68b685

10 files changed

Lines changed: 203 additions & 209 deletions

File tree

ffcx/codegeneration/C/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
from ffcx.codegeneration.C import expressions, file, form, integrals
44

5-
__all__ = ["expressions", "file", "form", "integrals", "suffixes"]
5+
from .formatter import Formatter
6+
7+
__all__ = ["Formatter", "expressions", "file", "form", "integrals", "suffixes"]

ffcx/codegeneration/C/expressions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from ffcx.codegeneration.backend import FFCXBackend
1616
from ffcx.codegeneration.C import expressions_template
17-
from ffcx.codegeneration.C.implementation import Formatter
17+
from ffcx.codegeneration.C.formatter import Formatter
1818
from ffcx.codegeneration.expression_generator import ExpressionGenerator
1919
from ffcx.codegeneration.utils import dtype_to_c_type, dtype_to_scalar_dtype
2020
from ffcx.ir.representation import ExpressionIR
@@ -44,8 +44,8 @@ def generator(ir: ExpressionIR, options):
4444
d["factory_name"] = factory_name
4545
parts = eg.generate()
4646

47-
CF = Formatter(options["scalar_type"])
48-
d["tabulate_expression"] = CF.format(parts)
47+
format = Formatter(options["scalar_type"])
48+
d["tabulate_expression"] = format(parts)
4949

5050
if len(ir.original_coefficient_positions) > 0:
5151
d["original_coefficient_positions"] = f"original_coefficient_positions_{factory_name}"
Lines changed: 72 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
# Copyright (C) 2023 Chris Richardson
1+
# Copyright (C) 2023-2025 Chris Richardson and Paul T. Kühner
22
#
33
# This file is part of FFCx. (https://www.fenicsproject.org)
44
#
55
# SPDX-License-Identifier: LGPL-3.0-or-later
66
"""C implementation."""
77

88
import warnings
9+
from functools import singledispatchmethod
910

1011
import numpy as np
1112
import numpy.typing as npt
1213

1314
import ffcx.codegeneration.lnodes as L
15+
from ffcx.codegeneration.interface import Formatter as FormatterInterface
1416
from ffcx.codegeneration.utils import dtype_to_c_type, dtype_to_scalar_dtype
1517

1618
math_table = {
@@ -142,7 +144,7 @@
142144
}
143145

144146

145-
class Formatter:
147+
class Formatter(FormatterInterface):
146148
"""C formatter."""
147149

148150
scalar_type: np.dtype
@@ -184,11 +186,18 @@ def _build_initializer_lists(self, values):
184186
arr += "}"
185187
return arr
186188

187-
def format_statement_list(self, slist) -> str:
189+
@singledispatchmethod
190+
def __call__(self, obj: L.LNode) -> str:
191+
"""Format an L Node."""
192+
raise NotImplementedError(f"Can not format object to type {type(obj)}")
193+
194+
@__call__.register
195+
def _(self, slist: L.StatementList) -> str:
188196
"""Format a statement list."""
189-
return "".join(self.format(s) for s in slist.statements)
197+
return "".join(self(s) for s in slist.statements)
190198

191-
def format_section(self, section) -> str:
199+
@__call__.register
200+
def _(self, section: L.Section) -> str:
192201
"""Format a section."""
193202
# add new line before section
194203
comments = (
@@ -197,28 +206,30 @@ def format_section(self, section) -> str:
197206
f"// Inputs: {', '.join(w.name for w in section.input)}\n"
198207
f"// Outputs: {', '.join(w.name for w in section.output)}\n"
199208
)
200-
declarations = "".join(self.format(s) for s in section.declarations)
209+
declarations = "".join(self(s) for s in section.declarations)
201210

202211
body = ""
203212
if len(section.statements) > 0:
204213
declarations += "{\n "
205-
body = "".join(self.format(s) for s in section.statements)
214+
body = "".join(self(s) for s in section.statements)
206215
body = body.replace("\n", "\n ")
207216
body = body[:-2] + "}\n"
208217

209218
body += "// ------------------------ \n"
210219
return str(comments + declarations + body)
211220

212-
def format_comment(self, c: L.Comment) -> str:
221+
@__call__.register
222+
def _(self, c: L.Comment) -> str:
213223
"""Format a comment."""
214-
return "// " + c.comment + "\n"
224+
return f"// {c.comment}\n"
215225

216-
def format_array_decl(self, arr) -> str:
226+
@__call__.register
227+
def _(self, arr: L.ArrayDecl) -> str:
217228
"""Format an array declaration."""
218229
dtype = arr.symbol.dtype
219230
typename = self._dtype_to_name(dtype)
220231

221-
symbol = self.format(arr.symbol)
232+
symbol = self(arr.symbol)
222233
dims = "".join([f"[{i}]" for i in arr.sizes])
223234
if arr.values is None:
224235
assert arr.const is False
@@ -228,23 +239,26 @@ def format_array_decl(self, arr) -> str:
228239
cstr = "static const " if arr.const else ""
229240
return f"{cstr}{typename} {symbol}{dims} = {vals};\n"
230241

231-
def format_array_access(self, arr) -> str:
242+
@__call__.register
243+
def _(self, arr: L.ArrayAccess) -> str:
232244
"""Format an array access."""
233-
name = self.format(arr.array)
234-
indices = f"[{']['.join(self.format(i) for i in arr.indices)}]"
245+
name = self(arr.array)
246+
indices = f"[{']['.join(self(i) for i in arr.indices)}]"
235247
return f"{name}{indices}"
236248

237-
def format_variable_decl(self, v) -> str:
249+
@__call__.register
250+
def _(self, v: L.VariableDecl) -> str:
238251
"""Format a variable declaration."""
239-
val = self.format(v.value)
240-
symbol = self.format(v.symbol)
252+
val = self(v.value)
253+
symbol = self(v.symbol)
241254
typename = self._dtype_to_name(v.symbol.dtype)
242255
return f"{typename} {symbol} = {val};\n"
243256

244-
def format_nary_op(self, oper) -> str:
257+
@__call__.register
258+
def _(self, oper: L.NaryOp) -> str:
245259
"""Format an n-ary operation."""
246260
# Format children
247-
args = [self.format(arg) for arg in oper.args]
261+
args = [self(arg) for arg in oper.args]
248262

249263
# Apply parentheses
250264
for i in range(len(args)):
@@ -254,11 +268,12 @@ def format_nary_op(self, oper) -> str:
254268
# Return combined string
255269
return f" {oper.op} ".join(args)
256270

257-
def format_binary_op(self, oper) -> str:
271+
@__call__.register
272+
def _(self, oper: L.BinOp) -> str:
258273
"""Format a binary operation."""
259274
# Format children
260-
lhs = self.format(oper.lhs)
261-
rhs = self.format(oper.rhs)
275+
lhs = self(oper.lhs)
276+
rhs = self(oper.rhs)
262277

263278
# Apply parentheses
264279
if oper.lhs.precedence >= oper.precedence:
@@ -269,52 +284,61 @@ def format_binary_op(self, oper) -> str:
269284
# Return combined string
270285
return f"{lhs} {oper.op} {rhs}"
271286

272-
def format_unary_op(self, oper) -> str:
287+
@__call__.register(L.Neg)
288+
@__call__.register(L.Not)
289+
def _(self, oper: L.Neg | L.Not) -> str:
273290
"""Format a unary operation."""
274-
arg = self.format(oper.arg)
291+
arg = self(oper.arg)
275292
if oper.arg.precedence >= oper.precedence:
276293
return f"{oper.op}({arg})"
277294
return f"{oper.op}{arg}"
278295

279-
def format_literal_float(self, val) -> str:
296+
@__call__.register
297+
def _(self, val: L.LiteralFloat) -> str:
280298
"""Format a literal float."""
281299
value = self._format_number(val.value)
282300
return f"{value}"
283301

284-
def format_literal_int(self, val) -> str:
302+
@__call__.register
303+
def _(self, val: L.LiteralInt) -> str:
285304
"""Format a literal int."""
286305
return f"{val.value}"
287306

288-
def format_for_range(self, r) -> str:
307+
@__call__.register
308+
def _(self, r: L.ForRange) -> str:
289309
"""Format a for loop over a range."""
290-
begin = self.format(r.begin)
291-
end = self.format(r.end)
292-
index = self.format(r.index)
310+
begin = self(r.begin)
311+
end = self(r.end)
312+
index = self(r.index)
293313
output = f"for (int {index} = {begin}; {index} < {end}; ++{index})\n"
294314
output += "{\n"
295-
body = self.format(r.body)
315+
body = self(r.body)
296316
for line in body.split("\n"):
297317
if len(line) > 0:
298318
output += f" {line}\n"
299319
output += "}\n"
300320
return output
301321

302-
def format_statement(self, s) -> str:
322+
@__call__.register
323+
def _(self, s: L.Statement) -> str:
303324
"""Format a statement."""
304-
return self.format(s.expr)
325+
return self(s.expr)
305326

306-
def format_assign(self, expr) -> str:
327+
@__call__.register(L.Assign)
328+
@__call__.register(L.AssignAdd)
329+
def _(self, expr: L.Assign | L.AssignAdd) -> str:
307330
"""Format an assignment."""
308-
rhs = self.format(expr.rhs)
309-
lhs = self.format(expr.lhs)
331+
rhs = self(expr.rhs)
332+
lhs = self(expr.lhs)
310333
return f"{lhs} {expr.op} {rhs};\n"
311334

312-
def format_conditional(self, s) -> str:
335+
@__call__.register
336+
def _(self, s: L.Conditional) -> str:
313337
"""Format a conditional."""
314338
# Format children
315-
c = self.format(s.condition)
316-
t = self.format(s.true)
317-
f = self.format(s.false)
339+
c = self(s.condition)
340+
t = self(s.true)
341+
f = self(s.false)
318342

319343
# Apply parentheses
320344
if s.condition.precedence >= s.precedence:
@@ -327,15 +351,18 @@ def format_conditional(self, s) -> str:
327351
# Return combined string
328352
return c + " ? " + t + " : " + f
329353

330-
def format_symbol(self, s) -> str:
354+
@__call__.register
355+
def _(self, s: L.Symbol) -> str:
331356
"""Format a symbol."""
332357
return f"{s.name}"
333358

334-
def format_multi_index(self, mi) -> str:
359+
@__call__.register
360+
def _(self, mi: L.MultiIndex) -> str:
335361
"""Format a multi-index."""
336-
return self.format(mi.global_index)
362+
return self(mi.global_index)
337363

338-
def format_math_function(self, c) -> str:
364+
@__call__.register
365+
def _(self, c: L.MathFunction) -> str:
339366
"""Format a mathematical function."""
340367
# Get a table of functions for this type, if available
341368
arg_type = self.scalar_type
@@ -349,48 +376,5 @@ def format_math_function(self, c) -> str:
349376

350377
# Get a function from the table, if available, else just use bare name
351378
func = dtype_math_table.get(c.function, c.function)
352-
args = ", ".join(self.format(arg) for arg in c.args)
379+
args = ", ".join(self(arg) for arg in c.args)
353380
return f"{func}({args})"
354-
355-
impl = {
356-
"Section": format_section,
357-
"StatementList": format_statement_list,
358-
"Comment": format_comment,
359-
"ArrayDecl": format_array_decl,
360-
"ArrayAccess": format_array_access,
361-
"MultiIndex": format_multi_index,
362-
"VariableDecl": format_variable_decl,
363-
"ForRange": format_for_range,
364-
"Statement": format_statement,
365-
"Assign": format_assign,
366-
"AssignAdd": format_assign,
367-
"Product": format_nary_op,
368-
"Neg": format_unary_op,
369-
"Sum": format_nary_op,
370-
"Add": format_binary_op,
371-
"Sub": format_binary_op,
372-
"Mul": format_binary_op,
373-
"Div": format_binary_op,
374-
"Not": format_unary_op,
375-
"LiteralFloat": format_literal_float,
376-
"LiteralInt": format_literal_int,
377-
"Symbol": format_symbol,
378-
"Conditional": format_conditional,
379-
"MathFunction": format_math_function,
380-
"And": format_binary_op,
381-
"Or": format_binary_op,
382-
"NE": format_binary_op,
383-
"EQ": format_binary_op,
384-
"GE": format_binary_op,
385-
"LE": format_binary_op,
386-
"GT": format_binary_op,
387-
"LT": format_binary_op,
388-
}
389-
390-
def format(self, s) -> str:
391-
"""Format as C."""
392-
name = s.__class__.__name__
393-
try:
394-
return self.impl[name](self, s)
395-
except KeyError:
396-
raise RuntimeError("Unknown statement: ", name)

ffcx/codegeneration/C/integrals.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from ffcx.codegeneration.backend import FFCXBackend
1616
from ffcx.codegeneration.C import integrals_template as ufcx_integrals
17-
from ffcx.codegeneration.C.implementation import Formatter
17+
from ffcx.codegeneration.C.formatter import Formatter
1818
from ffcx.codegeneration.integral_generator import IntegralGenerator
1919
from ffcx.codegeneration.utils import dtype_to_c_type, dtype_to_scalar_dtype
2020
from ffcx.ir.representation import IntegralIR
@@ -55,8 +55,8 @@ def generator(
5555
parts = ig.generate(domain)
5656

5757
# Format code as string
58-
CF = Formatter(options["scalar_type"]) # type: ignore
59-
body = CF.format(parts)
58+
format = Formatter(options["scalar_type"]) # type: ignore
59+
body = format(parts)
6060

6161
# Generate generic FFCx code snippets and add specific parts
6262
code = {}

ffcx/codegeneration/interface.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright (C) 2025 Paul T. Kühner
2+
#
3+
# This file is part of FFCx. (https://www.fenicsproject.org)
4+
#
5+
# SPDX-License-Identifier: LGPL-3.0-or-later
6+
7+
"""Backend interface declarations."""
8+
9+
import abc
10+
11+
from numpy import typing as npt
12+
13+
import ffcx.codegeneration.lnodes as L
14+
15+
16+
class Formatter(abc.ABC):
17+
"""Formatter interface."""
18+
19+
def __init__(self, dtype: npt.DTypeLike) -> None:
20+
"""Create."""
21+
raise NotImplementedError
22+
23+
def __call__(self, obj: L.LNode) -> str:
24+
"""Convert L-Node(s) to string representation."""
25+
raise NotImplementedError

ffcx/codegeneration/numba/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
from ffcx.codegeneration.numba import expressions, file, form, integrals
44

5-
__all__ = ["expressions", "file", "form", "integrals", "suffixes"]
5+
from .formatter import Formatter
6+
7+
__all__ = ["Formatter", "expressions", "file", "form", "integrals", "suffixes"]

ffcx/codegeneration/numba/expressions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ffcx.codegeneration.backend import FFCXBackend
1515
from ffcx.codegeneration.expression_generator import ExpressionGenerator
1616
from ffcx.codegeneration.numba import expressions_template
17-
from ffcx.codegeneration.numba.implementation import Formatter
17+
from ffcx.codegeneration.numba.formatter import Formatter
1818
from ffcx.ir.representation import ExpressionIR
1919

2020
logger = logging.getLogger("ffcx")
@@ -64,8 +64,8 @@ def generator(ir: ExpressionIR, options: dict[str, int | float | npt.DTypeLike])
6464
entity_local_index = numba.carray(_entity_local_index, ({size_local_index}))
6565
quadrature_permutation = numba.carray(_quadrature_permutation, ({size_permutation}))
6666
"""
67-
F = Formatter(options["scalar_type"]) # type: ignore
68-
body = F.format(parts)
67+
format = Formatter(options["scalar_type"]) # type: ignore
68+
body = format(parts)
6969
body = "\n".join([" " + line for line in body.split("\n")])
7070

7171
d["tabulate_expression"] = header + body

0 commit comments

Comments
 (0)