# (C) Copyright 2018- ECMWF.
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.

from dataclasses import FrozenInstanceError

import pytest
from pymbolic.primitives import Expression
from pydantic import ValidationError

from loki.backend import fgen
from loki.expression import symbols as sym, parse_expr
from loki.function import Function
from loki.ir import nodes as ir
from loki.subroutine import Subroutine
from loki.tools import as_tuple, flatten
from loki.types import BasicType, ProcedureType, Scope, SymbolAttributes


@pytest.fixture(name='scope')
def fixture_scope():
    return Scope()

@pytest.fixture(name='one')
def fixture_one():
    return sym.Literal(1)

@pytest.fixture(name='i')
def fixture_i(scope):
    return sym.Scalar('i', scope=scope)

@pytest.fixture(name='n')
def fixture_n(scope):
    return sym.Scalar('n', scope=scope)

@pytest.fixture(name='a_i')
def fixture_a_i(scope, i):
    return sym.Array('a', dimensions=(i,), scope=scope)

@pytest.fixture(name='a_n')
def fixture_a_n(scope, n):
    return sym.Array('a', dimensions=(n,), scope=scope)


def test_assignment(scope, a_i):
    """
    Test constructors of :any:`Assignment`.
    """
    assign = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    assert isinstance(assign.lhs, Expression)
    assert isinstance(assign.rhs, Expression)
    assert assign.comment is None

    # Ensure "frozen" status of node objects
    with pytest.raises(FrozenInstanceError):
        assign.lhs = sym.Scalar('b', scope=scope)
    with pytest.raises(FrozenInstanceError):
        assign.rhs = sym.Scalar('b', scope=scope)

    # Test errors for wrong contructor usage
    with pytest.raises(ValidationError):
        ir.Assignment(lhs='a', rhs=sym.Literal(42.0))
    with pytest.raises(ValidationError):
        ir.Assignment(lhs=a_i, rhs='42.0 + 6.0')
    with pytest.raises(ValidationError):
        ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0), comment=a_i)


def test_loop(scope, one, i, n, a_i):
    """
    Test constructors of :any:`Loop`.
    """
    assign = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    bounds = sym.Range((one, n))

    loop = ir.Loop(variable=i, bounds=bounds, body=(assign,))
    assert isinstance(loop.variable, Expression)
    assert isinstance(loop.bounds, Expression)
    assert isinstance(loop.body, tuple)
    assert all(isinstance(n, ir.Node) for n in loop.body)
    assert loop.children == ( i, bounds, (assign,) )

    # Ensure "frozen" status of node objects
    with pytest.raises(FrozenInstanceError):
        loop.variable = sym.Scalar('j', scope=scope)
    with pytest.raises(FrozenInstanceError):
        loop.bounds = sym.Range((n, sym.Scalar('k', scope=scope)))
    with pytest.raises(FrozenInstanceError):
        loop.body = (assign, assign, assign)

    # Test auto-casting of the body to tuple
    loop = ir.Loop(variable=i, bounds=bounds, body=assign)
    assert loop.body == (assign,)
    loop = ir.Loop(variable=i, bounds=bounds, body=( (assign,), ))
    assert loop.body == (assign,)
    loop = ir.Loop(variable=i, bounds=bounds, body=( assign, (assign,), assign, None))
    assert loop.body == (assign, assign, assign)

    # Test auto-casting with unnamed constructor args
    loop = ir.Loop(i, bounds, assign)
    assert loop.body == (assign,)
    loop = ir.Loop(i, bounds, [(assign,), None, assign])
    assert loop.body == (assign, assign)

    # Test errors for wrong contructor usage
    with pytest.raises(ValidationError):
        ir.Loop(variable=i, bounds=bounds, body=n)
    with pytest.raises(ValidationError):
        ir.Loop(variable=None, bounds=bounds, body=(assign,))
    with pytest.raises(ValidationError):
        ir.Loop(variable=i, bounds=None, body=(assign,))

    # TODO: Test pragmas, names and labels


def test_conditional(scope, n, a_i):
    """
    Test constructors of :any:`Conditional`.
    """
    assign = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    condition = parse_expr('i >= 2', scope=scope)

    cond = ir.Conditional(
        condition=condition, body=(assign,assign,), else_body=(assign,)
    )
    assert isinstance(cond.condition, Expression)
    assert isinstance(cond.body, tuple) and len(cond.body) == 2
    assert all(isinstance(n, ir.Node) for n in cond.body)
    assert isinstance(cond.else_body, tuple) and len(cond.else_body) == 1
    assert all(isinstance(n, ir.Node) for n in cond.else_body)
    assert cond.children == ( condition, (assign, assign), (assign,) )

    with pytest.raises(FrozenInstanceError):
        cond.condition = parse_expr('k == 0', scope=scope)
    with pytest.raises(FrozenInstanceError):
        cond.body = (assign, assign, assign)
    with pytest.raises(FrozenInstanceError):
        cond.else_body = (assign, assign, assign)

    # Test auto-casting of the body / else_body to tuple
    cond = ir.Conditional(condition=condition, body=assign)
    assert cond.body == (assign,) and not cond.else_body
    cond = ir.Conditional(condition=condition, body=( (assign,), ))
    assert cond.body == (assign,) and not cond.else_body
    cond = ir.Conditional(condition=condition, body=( assign, (assign,), assign, None))
    assert cond.body == (assign, assign, assign) and not cond.else_body

    cond = ir.Conditional(condition=condition, body=(), else_body=assign)
    assert not cond.body and cond.else_body == (assign,)
    cond = ir.Conditional(condition=condition, body=(), else_body=( (assign,), ))
    assert not cond.body and cond.else_body == (assign,)
    cond = ir.Conditional(
        condition=condition, body=(), else_body=( assign, (assign,), assign, None)
    )
    assert not cond.body and cond.else_body == (assign, assign, assign)

    # Test auto-casting with unnamed constructor args
    cond = ir.Conditional(condition)
    assert cond.body is () and not cond.else_body
    cond = ir.Conditional(condition, assign)
    assert cond.body == (assign,) and not cond.else_body
    cond = ir.Conditional(condition, body=[assign, (assign,)], else_body=[assign, None, (assign,)])
    assert cond.body == (assign, assign) and cond.else_body == (assign, assign)

    # TODO: Test inline, name, has_elseif


def test_conditional_nested(i, a_i):
    """
    Test nested chains of constructors of :any:`Conditional` to form
    multi-conditional.
    """
    multicond = ir.Conditional(
        condition=sym.Comparison(i, '==', sym.IntLiteral(1)),
        body=ir.Assignment(lhs=a_i, rhs=sym.Literal(1.0)),
        else_body=ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    )
    for idx in range(2, 4):
        multicond = ir.Conditional(
            condition=sym.Comparison(i, '==', sym.IntLiteral(idx)),
            body=ir.Assignment(lhs=a_i, rhs=sym.Literal(float(idx))),
            else_body=multicond, has_elseif=True
        )

    # Check that we can recover all bodies from a nested else-if construct
    else_bodies = multicond.else_bodies
    assert len(else_bodies) == 3
    assert all(isinstance(b, tuple) for b in else_bodies)
    assert isinstance(else_bodies[0][0], ir.Assignment)
    assert else_bodies[0][0].lhs == 'a(i)' and else_bodies[0][0].rhs == '2.0'
    assert isinstance(else_bodies[1][0], ir.Assignment)
    assert else_bodies[1][0].lhs == 'a(i)' and else_bodies[1][0].rhs == '1.0'
    assert isinstance(else_bodies[2][0], ir.Assignment)
    assert else_bodies[2][0].lhs == 'a(i)' and else_bodies[2][0].rhs == '42.0'

    # Not try without the final else
    multicond = ir.Conditional(
        condition=sym.Comparison(i, '==', sym.IntLiteral(1)),
        body=ir.Assignment(lhs=a_i, rhs=sym.Literal(1.0)),
    )
    for idx in range(2, 4):
        multicond = ir.Conditional(
            condition=sym.Comparison(i, '==', sym.IntLiteral(idx)),
            body=ir.Assignment(lhs=a_i, rhs=sym.Literal(float(idx))),
            else_body=multicond, has_elseif=True
        )
    else_bodies = multicond.else_bodies
    assert len(else_bodies) == 2
    assert all(isinstance(b, tuple) for b in else_bodies)
    assert isinstance(else_bodies[0][0], ir.Assignment)
    assert else_bodies[0][0].lhs == 'a(i)' and else_bodies[0][0].rhs == '2.0'
    assert isinstance(else_bodies[1][0], ir.Assignment)
    assert else_bodies[1][0].lhs == 'a(i)' and else_bodies[1][0].rhs == '1.0'


def test_section(n, a_n, a_i):
    """
    Test constructors and behaviour of :any:`Section` nodes.
    """
    assign = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    decl = ir.VariableDeclaration(symbols=(a_n,))
    func = Function(name='F', spec=(decl,), body=(assign,))

    # Test constructor for nodes and subroutine objects
    sec = ir.Section(body=(assign, assign))
    assert isinstance(sec.body, tuple) and len(sec.body) == 2
    assert all(isinstance(n, ir.Node) for n in sec.body)
    with pytest.raises(FrozenInstanceError):
        sec.body = (assign, assign)

    sec = ir.Section(body=(func, func))
    assert isinstance(sec.body, tuple) and len(sec.body) == 2
    assert all(isinstance(n, Scope) for n in sec.body)
    with pytest.raises(FrozenInstanceError):
        sec.body = (func, func)

    sec = ir.Section((assign, assign))
    assert sec.body == (assign, assign)

    # Test auto-casting of the body to tuple
    sec = ir.Section(body=assign)
    assert sec.body == (assign,)
    sec = ir.Section(body=( (assign,), ))
    assert sec.body == (assign,)
    sec = ir.Section(body=( assign, (assign,), assign, None))
    assert sec.body == (assign, assign, assign)
    sec = ir.Section((assign, (func,), assign, None))
    assert sec.body == (assign, func, assign)

    # Test auto-casting with unnamed constructor args
    sec = ir.Section()
    assert sec.body is ()
    sec = ir.Section([(assign,), assign, None, assign])
    assert sec.body == (assign, assign, assign)

    # Test prepend/insert/append additions
    sec = ir.Section(body=func)
    assert sec.body == (func,)
    sec.prepend(assign)
    assert sec.body == (assign, func)
    sec.append((assign, assign))
    assert sec.body == (assign, func, assign, assign)
    sec.insert(pos=3, node=func)
    assert sec.body == (assign, func, assign, func, assign)


def test_section_iterable(a_i):
    """
    Test iterable-style access for :any:`Section`.
    """
    assign1 = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    assign2 = ir.Assignment(lhs=a_i, rhs=sym.Literal(24.0))
    assign3 = ir.Assignment(lhs=a_i, rhs=sym.Literal(12.0))

    section = ir.Section(body=(assign1, assign2, assign1, assign3))
    assert list(section) == list(section.body)
    assert section[0] is assign1
    assert section[-1] is assign3
    assert assign1 in section
    assert len(section) == len(section.body) == 4
    assert section.index(assign1) == 0
    assert section.index(assign1, 1) == 2
    assert section.index(assign3, 0, 4) == 3
    assert as_tuple(section) == (section,)
    assert flatten([section]) == [section]


def test_loop_is_not_iterable(one, i, n, a_i):
    """
    Test that iterable access is not enabled for unrelated internal nodes.
    """
    assign = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    loop = ir.Loop(variable=i, bounds=sym.Range((one, n)), body=(assign,))

    with pytest.raises(TypeError):
        iter(loop)
    with pytest.raises(TypeError):
        _ = loop[0]
    with pytest.raises(TypeError):
        len(loop)


def test_associate_iterable(scope, a_i):
    """
    Test iterable-style access for :any:`Associate` while keeping it atomic.
    """
    b = sym.Scalar(name='b', scope=scope)
    b_a = sym.Array(name='a', parent=b, scope=scope)
    a = sym.Array(name='a', scope=scope)
    assign1 = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    assign2 = ir.Assignment(lhs=a_i.clone(parent=b), rhs=sym.Literal(66.6))

    assoc = ir.Associate(associations=((b_a, a),), body=(assign1, assign2), parent=scope)  # pylint: disable=unexpected-keyword-arg

    assert list(assoc) == list(assoc.body)
    assert assoc[0] is assign1
    assert assoc[-1] is assign2
    assert assign1 in assoc
    assert len(assoc) == len(assoc.body) == 2
    assert as_tuple(assoc) == (assoc,)
    assert flatten([assoc]) == [assoc]


def test_callstatement(scope, one, i, n, a_i):
    """ Test constructor of :any:`CallStatement` nodes. """

    cname = sym.ProcedureSymbol(name='test', scope=scope)
    call = ir.CallStatement(
        name=cname, arguments=(n, a_i), kwarguments=(('i', i), ('j', one))
    )
    assert isinstance(call.name, Expression)
    assert isinstance(call.arguments, tuple)
    assert all(isinstance(e, Expression) for e in call.arguments)
    assert isinstance(call.kwarguments, tuple)
    assert all(isinstance(e, tuple) for e in call.kwarguments)
    assert all(
        isinstance(k, str) and isinstance(v, Expression)
        for k, v in call.kwarguments
    )

    # Ensure "frozen" status of node objects
    with pytest.raises(FrozenInstanceError):
        call.name = sym.ProcedureSymbol('dave', scope=scope)
    with pytest.raises(FrozenInstanceError):
        call.arguments = (a_i, n, one)
    with pytest.raises(FrozenInstanceError):
        call.kwarguments = (('i', one), ('j', i))

    # Test auto-casting of the body to tuple
    call = ir.CallStatement(name=cname, arguments=[a_i, one])
    assert call.arguments == (a_i, one) and not call.kwarguments
    call = ir.CallStatement(name=cname, arguments=None)
    assert not call.arguments and not call.kwarguments
    call = ir.CallStatement(name=cname, kwarguments=[('i', i), ('j', one)])
    assert not call.arguments and call.kwarguments == (('i', i), ('j', one))
    call = ir.CallStatement(name=cname, kwarguments=None)
    assert not call.arguments and not call.kwarguments

    # Test auto-casting with unnamed constructor args
    call = ir.CallStatement(cname, a_i)
    assert call.arguments == (a_i,) and not call.kwarguments
    call = ir.CallStatement(cname, [a_i, one], [('i', i), ('j', one)])
    assert call.arguments == (a_i, one) and call.kwarguments == (('i', i), ('j', one))

    # Test errors for wrong contructor usage
    with pytest.raises(ValidationError):
        ir.CallStatement(name='a', arguments=(sym.Literal(42.0),))
    with pytest.raises(ValidationError):
        ir.CallStatement(name=cname, arguments=('a',))
    with pytest.raises(ValidationError):
        ir.Assignment(
            name=cname, arguments=(sym.Literal(42.0),), kwarguments=('i', 'i')
        )

    # TODO: Test pragmas, active and chevron


def test_callstatement_routine_types(scope):
    """Test type-checked access to call routine metadata."""

    callee = Subroutine(name='callee')
    cname = sym.ProcedureSymbol(
        name='callee', scope=scope, type=SymbolAttributes(dtype=ProcedureType(procedure=callee))
    )
    call = ir.CallStatement(name=cname)

    assert isinstance(call.procedure_type, ProcedureType)
    assert call.routine is callee

    unresolved = sym.ProcedureSymbol(name='unresolved')
    unresolved.type = SymbolAttributes(dtype=BasicType.DEFERRED)
    deferred_call = ir.CallStatement(name=unresolved)

    assert deferred_call.procedure_type is BasicType.DEFERRED
    assert deferred_call.routine is BasicType.DEFERRED
    assert not deferred_call.routine


def test_callstatement_invalid_routine_type():
    """Test runtime failure for invalid call routine type metadata."""

    cname = sym.ProcedureSymbol(name='broken')
    cname.type = SymbolAttributes(dtype=BasicType.DEFERRED)
    cname.type.dtype = object()
    call = ir.CallStatement(name=cname)

    with pytest.raises(TypeError, match=r'CallStatement\.name\.type\.dtype must be a DataType'):
        _ = call.procedure_type


def test_associate(scope, a_i):
    """
    Test constructors and scoping bahviour of :any:`Associate`.
    """
    b = sym.Scalar(name='b', scope=scope)
    b_a = sym.Array(name='a', parent=b, scope=scope)
    a = sym.Array(name='a', scope=scope)
    assign = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    assign2 = ir.Assignment(lhs=a_i.clone(parent=b), rhs=sym.Literal(66.6))

    assoc = ir.Associate(associations=((b_a, a),), body=(assign, assign2), parent=scope)  # pylint: disable=unexpected-keyword-arg
    assert isinstance(assoc.associations, tuple)
    assert all(isinstance(n, tuple) and len(n) == 2 for n in assoc.associations)
    assert isinstance(assoc.body, tuple)
    assert all(isinstance(n, ir.Node) for n in assoc.body)

    # TODO: Check constructor failures, auto-casting and frozen status

    # Check provided symbol maps
    assert 'B%a' in assoc.association_map and assoc.association_map['B%a'] is a
    assert b_a in assoc.association_map and assoc.association_map[b_a] is a
    assert 'a' in assoc.inverse_map and assoc.inverse_map['a'] is b_a
    assert a in assoc.inverse_map and assoc.inverse_map[a] is b_a

    # Check rescoping facility
    assert assign.lhs.scope is scope
    assert assign2.lhs.scope is scope
    assoc.rescope_symbols()
    assert assign.lhs.scope is assoc
    assert assign2.lhs.scope is scope


def test_multiconditional(scope, a_i, i):
    """
    Test constructors and scoping behaviour of :any:`MultiConditional`.
    """
    assign1 = ir.Assignment(lhs=a_i, rhs=sym.Literal(42.0))
    assign2 = ir.Assignment(lhs=a_i, rhs=sym.Literal(66.6))
    assign3 = ir.Assignment(lhs=a_i, rhs=sym.Literal(12.3))
    values = ((sym.Literal(1),), (sym.Literal(2),))
    multicond = ir.MultiConditional(
        expr=i, values=values, bodies=((assign1,), (assign2,)), else_body=(assign3,)
    )
    assert isinstance(multicond.expr, Expression)
    assert isinstance(multicond.values, tuple)
    assert all(isinstance(e, tuple) for e in multicond.values)
    assert all(
        all(isinstance(v, Expression) for v in val) for val in multicond.values
    )
    assert isinstance(multicond.bodies, tuple)
    assert all(isinstance(b, tuple) for b in multicond.bodies)
    assert all(
        all(isinstance(b, ir.Node) for b in body) for body in multicond.bodies
    )
    assert isinstance(multicond.else_body, tuple)
    assert all(isinstance(b, ir.Node) for b in multicond.else_body)
    assert multicond.children == (
        i, ((sym.Literal(1),), (sym.Literal(2),)), ((assign1,), (assign2,)), (assign3,)
    )

    # Ensure "frozen" status of node objects
    with pytest.raises(FrozenInstanceError):
        multicond.expr = parse_expr('k', scope=scope)
    with pytest.raises(FrozenInstanceError):
        multicond.values = ((sym.Literal(3),), (sym.Literal(4),))
    with pytest.raises(FrozenInstanceError):
        multicond.bodies = ((assign1,), (assign1,))
    with pytest.raises(FrozenInstanceError):
        multicond.else_body = (assign1, assign2)

    # Test auto-casting of the bodies and else_body to (nested) tuple(s)
    multicond = ir.MultiConditional(
        expr=i, values=sym.Literal(1), bodies=(()), else_body=()
    )
    assert multicond.values == ((sym.Literal(1),),)
    multicond = ir.MultiConditional(
        expr=i, values=((sym.Literal(1),),sym.Literal(2),), bodies=(()), else_body=()
    )
    assert multicond.values == ((sym.Literal(1),), (sym.Literal(2),))
    multicond = ir.MultiConditional(
        expr=i, values=values, bodies=assign1, else_body=()
    )
    assert multicond.bodies == ((assign1,),)
    multicond = ir.MultiConditional(
        expr=i, values=values, bodies=((assign1,), assign2), else_body=()
    )
    assert multicond.bodies == ((assign1,), (assign2,))
    multicond = ir.MultiConditional(
        expr=i, values=(()), bodies=(()), else_body=assign3
    )
    assert multicond.else_body == (assign3,)
    multicond = ir.MultiConditional(
        expr=i, values=(()), bodies=(()), else_body=((assign3,), assign2)
    )
    assert multicond.else_body == (assign3, assign2)


def test_stmt_nodes(n, i):
    """
    Test constructors and scoping behaviour of various :any:`GenericStmt` nodes.
    """

    assert fgen(ir.GenericStmt('Hello World')) == 'Hello World'
    with pytest.raises(ValidationError):
        ir.GenericStmt(n)

    # Potential symbol quantifiers
    assert fgen(ir.ImplicitStmt()) == 'IMPLICIT NONE'
    assert fgen(ir.ImplicitStmt('NONE')) == 'IMPLICIT NONE'
    assert fgen(ir.ImplicitStmt(i)) == 'IMPLICIT i'
    assert fgen(ir.ImplicitStmt((n, i))) == 'IMPLICIT n, i'

    assert fgen(ir.SaveStmt()) == 'SAVE'
    assert fgen(ir.SaveStmt(i)) == 'SAVE i'
    assert fgen(ir.SaveStmt((n, i))) == 'SAVE n, i'

    assert fgen(ir.PublicStmt()) == 'PUBLIC'
    assert fgen(ir.PublicStmt(i)) == 'PUBLIC i'
    assert fgen(ir.PublicStmt((n, i))) == 'PUBLIC n, i'

    assert fgen(ir.PrivateStmt()) == 'PRIVATE'
    assert fgen(ir.PrivateStmt(i)) == 'PRIVATE i'
    assert fgen(ir.PrivateStmt((n, i))) == 'PRIVATE n, i'

    assert fgen(ir.CommonStmt(i)) == 'COMMON i'
    assert fgen(ir.CommonStmt((n, i))) == 'COMMON n, i'
    with pytest.raises(ValidationError):
        ir.CommonStmt()  # Need at least one argument

    # Control flow statements
    assert fgen(ir.ContainsStmt()) == 'CONTAINS'
    with pytest.raises(ValidationError):
        ir.ContainsStmt(n)

    assert fgen(ir.ReturnStmt()) == 'RETURN'
    with pytest.raises(ValidationError):
        ir.ReturnStmt(n)

    assert fgen(ir.CycleStmt()) == 'CYCLE'
    with pytest.raises(ValidationError):
        ir.CycleStmt(n)

    assert fgen(ir.ContinueStmt()) == 'CONTINUE'
    with pytest.raises(ValidationError):
        ir.ContinueStmt(n)

    assert fgen(ir.StopStmt()) == 'STOP'
    assert fgen(ir.StopStmt(1)) == 'STOP 1'
    assert fgen(ir.StopStmt("'all done'")) == "STOP 'all done'"

    assert fgen(ir.ExitStmt()) == 'EXIT'
    assert fgen(ir.ExitStmt(1)) == 'EXIT 1'
    with pytest.raises(ValidationError):
        ir.ExitStmt('NoStringsAllowed')

    assert fgen(ir.GotoStmt('2345')) == 'GO TO 2345'
    with pytest.raises(ValidationError):
        assert ir.GotoStmt()

    # Output statements
    assert fgen(ir.PrintStmt(i)) == 'PRINT i'
    assert fgen(ir.PrintStmt((n, i))) == 'PRINT n, i'
    assert ir.PrintStmt((n, i)).values == (n, i)
    assert ir.PrintStmt((n, i)).text == 'n, i'
    with pytest.raises(ValidationError):
        ir.PrintStmt()  # Need at least one argument
    with pytest.raises(ValidationError, match='PrintStmt uses values='):
        ir.PrintStmt(text=(n, i))

    assert fgen(ir.FormatStmt()) == 'FORMAT'
    assert fgen(ir.FormatStmt(i)) == 'FORMAT(i)'
    assert fgen(ir.FormatStmt((n, i))) == 'FORMAT(n, i)'
    assert ir.FormatStmt((n, i)).values == (n, i)
    assert ir.FormatStmt((n, i)).text == 'n, i'
    with pytest.raises(ValidationError, match='FormatStmt uses values='):
        ir.FormatStmt(text=(n, i))

    # Check adding labels for certain control flow statements
    assert ir.ReturnStmt(label=42).label == '42'
    assert ir.ReturnStmt(label='666').label == '666'
    assert ir.ReturnStmt().label is None
    assert ir.CycleStmt(label=42).label == '42'
    assert ir.ContinueStmt(label=42).label == '42'
    assert ir.ExitStmt(label=42).label == '42'
    assert ir.StopStmt(label=42).label == '42'
