Skip to content

Expression API

mccode-antlr represents instrument parameter values and component parameter values as Expr objects — symbolic expression trees that emit either C or Python source code.

Quick reference

from mccode_antlr.common.expression import Expr, Value, DataType

# Literal constants
Expr.float(1.5)         # float constant
Expr.int(0)             # integer constant
Expr.str('"hello"')     # C string literal (note inner quotes)

# Identifiers and parameters
Expr.id("my_variable")          # generic identifier
Expr.parameter("E_i")           # instrument parameter (with prefix in C output)
Expr.parameter("n", DataType.int)  # with type hint

# Arithmetic (all Python operators work)
e = Expr.parameter("E_i") * 2 + Expr.float(0.1)
e = Expr.float(1.0) / (Expr.id("q") ** 2)

# Comparison expressions (for WHEN conditions)
cond = Expr.parameter("verbose").eq(1)      # verbose == 1
cond = Expr.parameter("mode").ne(0)         # mode != 0
cond = Expr.parameter("n").lt(10)           # n < 10
cond = Expr.parameter("n").gt(0)            # n > 0
cond = Expr.parameter("n").le(Expr.int(5))  # n <= 5
cond = Expr.parameter("n").ge(1)            # n >= 1

# Parsing from a C expression string
e = Expr.parse("2*PI*sin(a3*DEG2RAD)")

Why not Python's == operator?

Python's == is reserved for object identity comparison and returns a bool. Use .eq() / .ne() instead when you want to build an expression tree:

Expr.parameter("flag") == 1   # returns False (Python equality test)!
Expr.parameter("flag").eq(1)  # returns Expr(BinaryOp(...)) -- correct

Expr

mccode_antlr.common.expression.Expr

Bases: Struct

Source code in src/mccode_antlr/common/expression/expr.py
class Expr(Struct):
    expr: ExprNode

    @classmethod
    def from_dict(cls, args: dict):
        expr = args['expr']
        if not hasattr(expr, '__len__'):
            expr = [expr]
        return cls([value_or_op_from_dict(x) for x in expr])

    def __post_init__(self):
        if not isinstance(self.expr, list):
            self.expr = [self.expr]
        if any(not isinstance(node, ExprNodeSingular) for node in self.expr):
            types = list(dict.fromkeys(type(node) for node in self.expr).keys())
            raise ValueError(f"An Expr can not be a list of {types}")

    def __str__(self):
        return ','.join(str(x) for x in self.expr)

    def __format__(self, format_spec):
        """Abuse string formatting to append the _instrument_var.parameters prefix to parameter names"""
        return ','.join(format(x, format_spec) for x in self.expr)

    def __repr__(self):
        return ','.join(repr(x) for x in self.expr)

    def __hash__(self):
        return hash(str(self))

    def __contains__(self, value):
        return any(value in x for x in self.expr)

    @staticmethod
    def parse(s: str):
        from antlr4 import InputStream
        from ...grammar import McInstr_parse
        from ...instr import InstrVisitor
        visitor = InstrVisitor(None, None)
        return visitor.getExpr(McInstr_parse(InputStream(s), 'expr'))

    @classmethod
    def float(cls, value):
        if isinstance(value, cls):
            for expr in value.expr:
                if expr.data_type != DataType.float:
                    expr.data_type = DataType.float
            return value
        return cls(Value.float(value))

    @classmethod
    def int(cls, value):
        if isinstance(value, cls):
            for expr in value.expr:
                if expr.data_type != DataType.int:
                    expr.data_type = DataType.int
            return value
        return cls(Value.int(value))

    @classmethod
    def str(cls, value):
        if isinstance(value, cls):
            for expr in value.expr:
                if expr.data_type != DataType.str:
                    expr.data_type = DataType.str
            return value
        return cls(Value.str(value))

    @classmethod
    def id(cls, value):
        if isinstance(value, cls):
            return value
        return cls(Value.id(value))

    @classmethod
    def parameter(cls, value: str, dt=None) -> 'Expr':
        """Create an Expr for a known instrument parameter (ObjectType.parameter).

        Unlike Expr.id(), the parameter flag is set immediately so that the
        ``_instrument_var._parameters.`` prefix is used in C output without waiting
        for verify_parameters() to be called during instrument finalisation.

        Args:
            value: parameter name string, or an existing Expr (returned unchanged)
            dt: optional DataType hint (default: DataType.undefined)

        Returns:
            Expr wrapping a Value with ObjectType.parameter
        """
        if isinstance(value, cls):
            return value
        return cls(Value.parameter(value, dt))

    @classmethod
    def array(cls, value):
        return cls(Value.array(value))

    @classmethod
    def function(cls, value):
        return cls(Value.function(value))

    @classmethod
    def best(cls, value):
        return cls(Value.best(value))

    @property
    def is_singular(self):
        return len(self.expr) == 1

    @property
    def is_op(self):
        return len(self.expr) == 1 and self.expr[0].is_op

    @property
    def is_zero(self):
        return len(self.expr) == 1 and self.expr[0].is_zero

    @property
    def is_id(self):
        return len(self.expr) == 1 and self.expr[0].is_id

    @property
    def is_parameter(self):
        return len(self.expr) == 1 and self.expr[0].is_parameter

    @property
    def is_str(self):
        return len(self.expr) == 1 and self.expr[0].is_str

    @property
    def is_scalar(self):
        return len(self.expr) == 1 and self.expr[0].is_scalar

    def is_value(self, value):
        return len(self.expr) == 1 and self.expr[0].is_value(value)

    @property
    def is_vector(self):
        return len(self.expr) == 1 and self.expr[0].is_vector

    @property
    def vector_len(self):
        if len(self.expr) != 1:
            raise RuntimeError('No vector_len for array Expr objects')
        return self.expr[0].vector_len

    @property
    def is_constant(self):
        return len(self.expr) == 1 and isinstance(self.expr[0], Value) and not self.expr[0].is_id

    @property
    def has_value(self):
        return self.is_constant and self.expr[0].has_value

    @property
    def vector_known(self):
        return self.is_constant and self.expr[0].vector_known

    @property
    def value(self):
        if not self.is_constant:
            raise NotImplementedError("No conversion from expressions to constants ... yet")
        return self.expr[0].value

    @property
    def first(self):
        return self.expr[0]

    @property
    def last(self):
        return self.expr[-1]

    def compatible(self, other, id_ok=False):
        other_expr = other.expr[0] if (isinstance(other, Expr) and len(other.expr) == 1) else other
        return len(self.expr) == 1 and self.expr[0].compatible(other_expr, id_ok)

    def _prep_cmp(self, other):
        """Normalise `other` to a single Value/Op node for comparison expression building."""
        if len(self.expr) != 1:
            raise RuntimeError('Can not build comparison expression for array Expr')
        if isinstance(other, Expr):
            if len(other.expr) != 1:
                raise RuntimeError('Can not build comparison expression against array Expr')
            return other.expr[0]
        if not isinstance(other, (Value, BinaryOp, UnaryOp, TrinaryOp)):
            return Value.best(other)
        return other

    def eq(self, other) -> 'Expr':
        """Return Expr for `self == other` (always an expression tree, never a bool)."""
        return Expr(self.expr[0].eq(self._prep_cmp(other)))

    def ne(self, other) -> 'Expr':
        """Return Expr for `self != other`."""
        return Expr(self.expr[0].ne(self._prep_cmp(other)))

    def lt(self, other) -> 'Expr':
        """Return Expr for `self < other`."""
        return Expr(self.expr[0] < self._prep_cmp(other))

    def gt(self, other) -> 'Expr':
        """Return Expr for `self > other`."""
        return Expr(self.expr[0] > self._prep_cmp(other))

    def le(self, other) -> 'Expr':
        """Return Expr for `self <= other`."""
        return Expr(self.expr[0] <= self._prep_cmp(other))

    def ge(self, other) -> 'Expr':
        """Return Expr for `self >= other`."""
        return Expr(self.expr[0] >= self._prep_cmp(other))

    def _prep_numeric_operation(self, msg: str, other):
        if len(self.expr) != 1:
            raise RuntimeError(f'Can not {msg} array Expr')
        return other.expr[0] if (isinstance(other, Expr) and len(other.expr) == 1) else other

    def _prep_rev_numeric_operation(self, msg: str, other):
        r = self._prep_numeric_operation(msg, other)
        if not isinstance(r, (Expr, TrinaryOp, BinaryOp, UnaryOp, Value)):
            r = Value.best(r)
        return r

    def __add__(self, other):
        return Expr(self.expr[0] + self._prep_numeric_operation('add to', other))

    def __sub__(self, other):
        return Expr(self.expr[0] - self._prep_numeric_operation('subtract', other))

    def __mul__(self, other):
        return Expr(self.expr[0] * self._prep_numeric_operation('multiply', other))

    def __mod__(self, other):
        return Expr(self.expr[0] % self._prep_numeric_operation('mod', other))

    def __truediv__(self, other):
        return Expr(self.expr[0] / self._prep_numeric_operation('divide', other))

    def __floordiv__(self, other):
        return Expr(self.expr[0] // self._prep_numeric_operation('divide', other))

    def __pow__(self, other):
        return Expr(self.expr[0] ** self._prep_numeric_operation('raise', other))

    def __radd__(self, other):
        return Expr(self._prep_rev_numeric_operation('add to', other) + self.expr[0])

    def __rsub__(self, other):
        return Expr(self._prep_rev_numeric_operation('subtract', other) - self.expr[0])

    def __rmul__(self, other):
        return Expr(self._prep_rev_numeric_operation('multiply', other) * self.expr[0])

    def __rtruediv__(self, other):
        return Expr(self._prep_rev_numeric_operation('divide', other) / self.expr[0])

    def __rfloordiv__(self, other):
        return Expr(self._prep_rev_numeric_operation('divide', other) // self.expr[0])

    def __rpow__(self, other):
        return Expr(self._prep_rev_numeric_operation('raise', other) ** self.expr[0])

    def __neg__(self):
        return Expr([-x for x in self.expr])

    def __pos__(self):
        return self

    def __abs__(self):
        return Expr([abs(x) for x in self.expr])

    def __round__(self, n=None):
        return Expr([round(x, n) for x in self.expr])

    def __eq__(self, other):
        if isinstance(other, Expr):
            if len(other.expr) != len(self.expr):
                return False
            for o_expr, s_expr in zip(other.expr, self.expr):
                if o_expr != s_expr:
                    return False
            return True
        return len(self.expr) == 1 and self.expr[0] == other

    def __lt__(self, other):
        if isinstance(other, Expr):
            if len(other.expr) != len(self.expr):
                raise RuntimeError('Can not compare unequal-sized-array Expr objects')
            for o_expr, s_expr in zip(other.expr, self.expr):
                if o_expr <= s_expr:
                    return False
            return True
        return len(self.expr) == 1 and self.expr[0] < other

    def __gt__(self, other):
        if isinstance(other, Expr):
            if len(other.expr) != len(self.expr):
                raise RuntimeError('Can not compare unequal-sized-array Expr objects')
            for o_expr, s_expr in zip(other.expr, self.expr):
                if o_expr >= s_expr:
                    return False
            return True
        return len(self.expr) == 1 and self.expr[0] > other

    def __le__(self, other):
        if isinstance(other, Expr):
            if len(other.expr) != len(self.expr):
                raise RuntimeError('Can not compare unequal-sized-array Expr objects')
            for o_expr, s_expr in zip(other.expr, self.expr):
                if o_expr < s_expr:
                    return False
            return True
        return len(self.expr) == 1 and self.expr[0] <= other

    def __ge__(self, other):
        if isinstance(other, Expr):
            if len(other.expr) != len(self.expr):
                raise RuntimeError('Can not compare unequal-sized-array Expr objects')
            for o_expr, s_expr in zip(other.expr, self.expr):
                if o_expr > s_expr:
                    return False
            return True
        return len(self.expr) == 1 and self.expr[0] >= other

    def __int__(self):
        if not len(self.expr) == 1:
            raise RuntimeError('No conversion to int for array Expr objects')
        return int(self.expr[0])

    @property
    def mccode_c_type(self):
        if len(self.expr) != 1:
            raise RuntimeError('No McCode C type for array Expr objects')
        if not isinstance(self.expr[0], Value):
            logger.critical(f'Why is {self.expr[0]} not a Value?')
        return self.expr[0].mccode_c_type

    @property
    def mccode_c_type_name(self):
        if len(self.expr) != 1:
            raise RuntimeError('No McCode C type name for array Expr objects')
        return self.expr[0].mccode_c_type_name

    @property
    def data_type(self):
        if len(self.expr) != 1:
            raise RuntimeError('No data type for array Expr objects')
        return self.expr[0].data_type

    @data_type.setter
    def data_type(self, dt):
        if len(self.expr) != 1:
            raise RuntimeError('No data type for array Expr objects')
        self.expr[0].data_type = dt

    @property
    def shape_type(self):
        if len(self.expr) != 1:
            raise RuntimeError('No data type for array Expr objects')
        return self.expr[0].shape_type

    @shape_type.setter
    def shape_type(self, st):
        if len(self.expr) != 1:
            raise RuntimeError('No data type for array Expr objects')
        if not isinstance(self.expr[0], Value):
            raise RuntimeError('No data type for non-scalar-Value Expr objects')
        self.expr[0].shape_type = st

    def simplify(self):
        """Perform a very basic analysis to reduce the expression complexity"""
        def simplify_to_single_or_list(node):
            s = node.simplify()
            return s[0] if hasattr(s, '__len__') and len(s) == 1 else s
        return Expr([simplify_to_single_or_list(x) for x in self.expr])

    def evaluate(self, known: dict):
        def evaluate_to_single_or_list(node):
            s = node.evaluate(known)
            return s[0] if hasattr(s, '__len__') and len(s) == 1 else s
        return Expr([evaluate_to_single_or_list(x) for x in self.expr]).simplify()

    def depends_on(self, name: str):
        return any(x.depends_on(name) for x in self.expr)

    def copy(self):
        return Expr([x.copy() for x in self.expr])

    def verify_parameters(self, instrument_parameter_names: list[str]):
        for x in self.expr:
            x.verify_parameters(instrument_parameter_names)

__format__(format_spec)

Abuse string formatting to append the _instrument_var.parameters prefix to parameter names

Source code in src/mccode_antlr/common/expression/expr.py
def __format__(self, format_spec):
    """Abuse string formatting to append the _instrument_var.parameters prefix to parameter names"""
    return ','.join(format(x, format_spec) for x in self.expr)

parameter(value, dt=None) classmethod

Create an Expr for a known instrument parameter (ObjectType.parameter).

Unlike Expr.id(), the parameter flag is set immediately so that the _instrument_var._parameters. prefix is used in C output without waiting for verify_parameters() to be called during instrument finalisation.

Parameters:

Name Type Description Default
value str

parameter name string, or an existing Expr (returned unchanged)

required
dt

optional DataType hint (default: DataType.undefined)

None

Returns:

Type Description
'Expr'

Expr wrapping a Value with ObjectType.parameter

Source code in src/mccode_antlr/common/expression/expr.py
@classmethod
def parameter(cls, value: str, dt=None) -> 'Expr':
    """Create an Expr for a known instrument parameter (ObjectType.parameter).

    Unlike Expr.id(), the parameter flag is set immediately so that the
    ``_instrument_var._parameters.`` prefix is used in C output without waiting
    for verify_parameters() to be called during instrument finalisation.

    Args:
        value: parameter name string, or an existing Expr (returned unchanged)
        dt: optional DataType hint (default: DataType.undefined)

    Returns:
        Expr wrapping a Value with ObjectType.parameter
    """
    if isinstance(value, cls):
        return value
    return cls(Value.parameter(value, dt))

eq(other)

Return Expr for self == other (always an expression tree, never a bool).

Source code in src/mccode_antlr/common/expression/expr.py
def eq(self, other) -> 'Expr':
    """Return Expr for `self == other` (always an expression tree, never a bool)."""
    return Expr(self.expr[0].eq(self._prep_cmp(other)))

ne(other)

Return Expr for self != other.

Source code in src/mccode_antlr/common/expression/expr.py
def ne(self, other) -> 'Expr':
    """Return Expr for `self != other`."""
    return Expr(self.expr[0].ne(self._prep_cmp(other)))

lt(other)

Return Expr for self < other.

Source code in src/mccode_antlr/common/expression/expr.py
def lt(self, other) -> 'Expr':
    """Return Expr for `self < other`."""
    return Expr(self.expr[0] < self._prep_cmp(other))

gt(other)

Return Expr for self > other.

Source code in src/mccode_antlr/common/expression/expr.py
def gt(self, other) -> 'Expr':
    """Return Expr for `self > other`."""
    return Expr(self.expr[0] > self._prep_cmp(other))

le(other)

Return Expr for self <= other.

Source code in src/mccode_antlr/common/expression/expr.py
def le(self, other) -> 'Expr':
    """Return Expr for `self <= other`."""
    return Expr(self.expr[0] <= self._prep_cmp(other))

ge(other)

Return Expr for self >= other.

Source code in src/mccode_antlr/common/expression/expr.py
def ge(self, other) -> 'Expr':
    """Return Expr for `self >= other`."""
    return Expr(self.expr[0] >= self._prep_cmp(other))

simplify()

Perform a very basic analysis to reduce the expression complexity

Source code in src/mccode_antlr/common/expression/expr.py
def simplify(self):
    """Perform a very basic analysis to reduce the expression complexity"""
    def simplify_to_single_or_list(node):
        s = node.simplify()
        return s[0] if hasattr(s, '__len__') and len(s) == 1 else s
    return Expr([simplify_to_single_or_list(x) for x in self.expr])

Value

mccode_antlr.common.expression.Value

Bases: Struct

Source code in src/mccode_antlr/common/expression/nodes.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
class Value(Struct, tag_field='type', tag='value'):
    _value: _ValueScalar
    _data: DataType = DataType.undefined
    _object: Optional[ObjectType] = None
    _shape: Optional[ShapeType] = None

    def __post_init__(self):
        if self._object is None:
            self._object = ObjectType.identifier if self._data != DataType.str and isinstance(self._value, str) else ObjectType.value
        if self._shape is None:
            self._shape = ShapeType.vector if not isinstance(self._value, str) and hasattr(self._value, '__len__') else ShapeType.scalar

        for prop, typ in zip(('_data', '_object', '_shape'), (DataType, ObjectType, ShapeType)):
            if not isinstance(getattr(self, prop), typ):
                setattr(self, prop, typ(getattr(self, prop)))

    def __int__(self):
        if self.data_type != DataType.int:
            raise RuntimeError('Non-integer data type Value; round first')
        return self._value

    @property
    def value(self):
        return self._value

    @property
    def object_type(self):
        return self._object

    @property
    def data_type(self):
        return self._data

    @property
    def shape_type(self):
        return self._shape

    @shape_type.setter
    def shape_type(self, st):
        if not isinstance(st, ShapeType):
            raise RuntimeError('Non ShapeType value set for shape_type')
        if st.is_vector:
            if self.is_str:
                raise RuntimeError('No support for vectors of strings, e.g. char**')
            if not self.is_id or not hasattr(self.value, '__len__'):
                raise RuntimeError('Can not make a scalar value have vector type unless it is an identifier')
            self._shape = st
        else:
            if not (self.is_str or self.is_id) and hasattr(self.value, '__len__'):
                raise RuntimeError('Can not make vector value have scalar type')
            self._shape = st

    @value.setter
    def value(self, value):
        logger.debug(f'Updating Value from {self._value} to {value}')
        self._value = value

    @data_type.setter
    def data_type(self, dt):
        self._data = dt

    @property
    def has_value(self):
        return self.value is not None

    @property
    def vector_known(self):
        return self.is_vector and self.has_value and not isinstance(self.value, str)

    def special_str(self, prefix=None):
        if prefix is None:
            prefix = "_instrument_var._parameters."
        return f'{prefix}{self.value}' if self.is_parameter else f'{self.value}'

    def _str_repr_(self):
        return str(self.value)

    def __str__(self):
        return self._str_repr_()

    def __format__(self, format_spec):
        """Abuse string format specifications to prepend the _instrument_var._parameters. prefix to parameters"""
        if format_spec == 'p':
            return self.special_str()
        elif format_spec.startswith('prefix:'):
            return self.special_str(format_spec[7:])
        return self._str_repr_()

    def __repr__(self):
        return f'{self.shape_type} {self.data_type} {self._str_repr_()}'

    def __hash__(self):
        return hash(str(self))

    def compatible(self, other, id_ok=False):
        from .expr import Expr
        if isinstance(other, Expr) and other.is_singular:
            other = other.expr[0]
        if isinstance(other, (UnaryOp, BinaryOp)):
            return id_ok
        value = other if isinstance(other, Value) else Value.best(other)
        return (id_ok and value.is_str) or (self.data_type.compatible(value.data_type) and self.shape_type.compatible(value.shape_type))

    @classmethod
    def float(cls, value):
        try:
            v = float(value) if value is not None else None
        except ValueError:
            v = value
        return cls(v, DataType.float, ObjectType.value, ShapeType.scalar)

    @classmethod
    def int(cls, value):
        try:
            v = int(value) if value is not None else None
        except ValueError:
            v = value
        return cls(v, DataType.int, ObjectType.value, ShapeType.scalar)

    @classmethod
    def str(cls, value):
        return cls(value, DataType.str, ObjectType.value, ShapeType.scalar)

    @classmethod
    def id(cls, value):
        return cls(value, DataType.undefined, ObjectType.identifier, ShapeType.scalar)

    @classmethod
    def parameter(cls, value: str, dt: 'DataType | None' = None) -> 'Value':
        """Create a Value representing a known instrument parameter (ObjectType.parameter).

        Unlike Value.id(), the ObjectType.parameter flag is set immediately so that
        format(..., 'p') emits the _instrument_var._parameters. prefix without waiting
        for verify_parameters() to be called.

        Args:
            value: the parameter name
            dt: optional DataType hint (default: DataType.undefined)
        """
        return cls(value, dt if dt is not None else DataType.undefined, ObjectType.parameter, ShapeType.scalar)

    @classmethod
    def array(cls, value, dt: DataType | None = None):
        return cls(value, dt if dt is not None else DataType.undefined, None, ShapeType.vector)

    @classmethod
    def function(cls, value, dt: DataType | None = None):
        return cls(value, dt if dt is not None else DataType.undefined, ObjectType.function)

    @classmethod
    def best(cls, value):
        if isinstance(value, str) and value[0] == '"' and value[-1] == '"':
            return cls(value, DataType.str)
        elif isinstance(value, str):
            # Any string value which is not wrapped in double quotes must(?) be an identifier
            return cls(value, DataType.undefined, ObjectType.identifier, ShapeType.unknown)
        if isinstance(value, int) or (isinstance(value, float) and value.is_integer()):
            return cls(value, DataType.int)
        return cls(value, DataType.float)

    @property
    def is_id(self):
        # FIXME 2023-10-16 Should instrument parameters not also be identifiers?
        return self.object_type == ObjectType.identifier or self.object_type == ObjectType.parameter

    @property
    def is_constant(self):
        return not self.is_id

    @property
    def is_parameter(self):
        return self.object_type.is_parameter

    @property
    def is_str(self):
        return self.object_type == ObjectType.value and self.data_type.is_str

    @property
    def is_float(self):
        return self.data_type == DataType.float

    @property
    def is_int(self):
        return self.data_type == DataType.int

    @property
    def is_op(self):
        return False

    @property
    def is_zero(self):
        if self.is_id:
            return False
        if self.is_str:
            # This is not great, but captures a case where, e.g., -1 is interpreted as an empty string minus 1
            return len(self.value.strip('"')) == 0
        return self.value == 0

    def is_value(self, v):
        return not self.is_id and (v.is_value(self.value) if hasattr(v, 'is_value') else self.value == v)

    @property
    def is_scalar(self):
        return self.shape_type.is_scalar

    @property
    def is_vector(self):
        return self.shape_type.is_vector

    @property
    def vector_len(self):
        return len(self.value) if self.is_vector else 1

    def as_type(self, dt: DataType):
        return Value(self.value, dt, self.object_type, self.shape_type)

    def __add__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        if self.is_zero:
            return other
        if other.is_zero:
            return self
        if other.is_op and isinstance(other, UnaryOp) and other.op == '-' and len(other.value) == 1:
            return self - other.value[0]
        if self.is_id and (isinstance(other, Value) and not isinstance(other.value, str) and other < 0):
            return self - (-other.value)
        if other.is_op or self.is_id or other.is_id or not self.is_constant or not other.is_constant:
            return BinaryOp(self.data_type, OpStyle.C, '+', [self], [other])
        pdt = self.data_type + other.data_type
        return BinaryOp(self.data_type, OpStyle.C, '+', [self], [other]) if pdt.is_str else Value(self.value + other.value, pdt)

    def __sub__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        if self.is_zero:
            return -other
        if other.is_zero:
            return self
        if other.is_op and isinstance(other, UnaryOp) and other.op == '-' and len(other.value) == 1:
            return self + other.value[0]
        if self.is_id and (isinstance(other, Value) and not isinstance(other.value, str) and other < 0):
            return self + (-other.value)
        if other.is_op or self.is_id or other.is_id or not self.is_constant or not other.is_constant:
            return BinaryOp(self.data_type, OpStyle.C, '-', [self], [other])
        pdt = self.data_type - other.data_type
        return BinaryOp(self.data_type, OpStyle.C, '-', [self], [other]) if pdt.is_str else Value(self.value - other.value, pdt)

    def __mul__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        pdt = self.data_type * other.data_type
        if self.is_zero or other.is_zero:
            return Value(0, DataType.int if pdt.is_str else pdt)
        if self.is_value(1):
            return other.as_type(pdt)
        if self.is_value(-1):
            return (-other).as_type(pdt)
        if other.is_value(1):
            return self.as_type(pdt)
        if other.is_value(-1):
            return (-self).as_type(pdt)
        if other.is_op or self.is_id or other.is_id:
            return BinaryOp(self.data_type, OpStyle.C, '*', [self], [other])
        return BinaryOp(self.data_type, OpStyle.C, '*', [self], [other]) if pdt.is_str else Value(self.value * other.value, pdt)

    def __mod__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        pdt = self.data_type
        if other.is_op or self.is_id or other.is_id or pdt.is_str:
            return BinaryOp(self.data_type, OpStyle.C, '%', [self], [other])
        return Value(self.value % other.value, pdt)

    def __truediv__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        pdt = self.data_type / other.data_type
        if self.is_zero:
            return Value(0, DataType.int if pdt.is_str else pdt)
        if other.is_value(1):
            return self.as_type(pdt)
        if other.is_value(-1):
            return (-self).as_type(pdt)
        if other.is_zero:
            raise RuntimeError('Division by zero!')
        if other.is_op or self.is_id or other.is_id:
            return BinaryOp(self.data_type, OpStyle.C, '/', [self], [other])
        return BinaryOp(self.data_type, OpStyle.C, '/', [self], [other]) if pdt.is_str else Value(self.value / other.value, pdt)

    def __floordiv__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        pdt = self.data_type // other.data_type
        if self.is_zero:
            return Value(0, DataType.int)
        if other.is_value(1):
            return Value.int(round(self.value))
        if other.is_value(-1):
            return -Value.int(round(self.value))
        if other.is_zero:
            raise RuntimeError('Division by zero!')
        if other.is_op or self.is_id or other.is_id:
            return BinaryOp(self.data_type, OpStyle.C, '//', [self], [other])
        return BinaryOp(self.data_type, OpStyle.C, '//', [self], [other]) if pdt.is_str else Value.int(self.value // other.value)

    def __neg__(self):
        return UnaryOp(self.data_type, OpStyle.C, '-', [self]) if self.is_id or self.data_type.is_str else Value(-self.value, self.data_type)

    def __pos__(self):
        return Value(self.value, self.data_type)

    def __abs__(self):
        return UnaryOp(self.data_type, OpStyle.C, 'abs', [self]) if self.is_id or self.data_type.is_str else Value(abs(self.value), self.data_type)

    def __round__(self, n=None):
        return UnaryOp(self.data_type, OpStyle.C, 'round', [self]) if self.is_id or self.data_type.is_str \
            else Value(round(self.value, n), self.data_type)

    def __eq__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        if other.is_op:
            return False
        return self.value == other.value

    def eq(self, other) -> 'BinaryOp':
        """Return a BinaryOp expression node for `self == other` (never a bool)."""
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        return BinaryOp(self.data_type, OpStyle.C, '__eq__', [self], [other])

    def ne(self, other) -> 'BinaryOp':
        """Return a BinaryOp expression node for `self != other`."""
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        return BinaryOp(self.data_type, OpStyle.C, '__neq__', [self], [other])

    def __lt__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        if self.is_id or other.is_op or other.is_id:
            return BinaryOp(self.data_type, OpStyle.C, '__lt__', [self], [other])
        return self.value < other.value

    def __gt__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        if self.is_id or other.is_op or other.is_id:
            return BinaryOp(self.data_type, OpStyle.C, '__gt__', [self], [other])
        return self.value > other.value

    def __le__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        if self.is_id or other.is_op or other.is_id:
            return BinaryOp(self.data_type, OpStyle.C, '__le__', [self], [other])
        return self.value <= other.value

    def __ge__(self, other):
        other = other if isinstance(other, (Value, Op)) else Value.best(other)
        if self.is_id or other.is_op or other.is_id:
            return BinaryOp(self.data_type, OpStyle.C, '__ge__', [self], [other])
        return self.value >= other.value

    def __pow__(self, power):
        if not isinstance(power, (Value, Op)):
            power = Value.best(power)
        if self.is_zero or self.is_value(1):
            return self
        if power.is_zero:
            return Value(1, self.data_type)
        if self.is_constant and power.is_constant:
            return Value(_value=self.value ** power.value, _data=self.data_type)
        return BinaryOp(self.data_type, OpStyle.C, '__pow__', [self], [power])

    @property
    def mccode_c_type(self):
        return self.data_type.mccode_c_type + self.shape_type.mccode_c_type

    @property
    def mccode_c_type_name(self):
        if self.data_type == DataType.float and self.shape_type == ShapeType.scalar:
            return "instr_type_double"
        if self.data_type == DataType.int and self.shape_type == ShapeType.scalar:
            return "instr_type_int"
        if self.data_type == DataType.str and self.shape_type == ShapeType.scalar:
            return "instr_type_string"
        if self.data_type == DataType.float and self.shape_type == ShapeType.vector:
            return "instr_type_vector"
        if self.data_type == DataType.int and self.shape_type == ShapeType.vector:
            return "instr_type_vector"
        raise RuntimeError(f"No known conversion from non-enumerated data type {self.data_type} + {self.shape_type}")

    def simplify(self):
        return self

    def evaluate(self, known: dict):
        if not self.is_constant and self.value in known:
            from .expr import Expr
            result = known[self.value]
            if isinstance(result, Expr) and result.is_singular:
                return result.expr[0]
            return result
        return self

    def depends_on(self, name: str):
        return not self.is_constant and self.value == name

    def copy(self):
        return Value(self.value, self.data_type, self.object_type, self.shape_type)

    def __contains__(self, value):
        if self.is_id and isinstance(value, (str, Value)):
            return self.value == value
        if self.is_vector:
            return value in self.value
        if self.is_str and isinstance(value, str) and (value[0] != '"' or value[-1] != '"'):
            # string Values are always wrapped in double quotes
            return self.value.strip('"') == value.strip('"')
        return self.value == value

    def verify_parameters(self, instrument_parameter_names: list[str]):
        if self.is_id and self.value in instrument_parameter_names:
            self._object = ObjectType.parameter

__format__(format_spec)

Abuse string format specifications to prepend the _instrument_var._parameters. prefix to parameters

Source code in src/mccode_antlr/common/expression/nodes.py
def __format__(self, format_spec):
    """Abuse string format specifications to prepend the _instrument_var._parameters. prefix to parameters"""
    if format_spec == 'p':
        return self.special_str()
    elif format_spec.startswith('prefix:'):
        return self.special_str(format_spec[7:])
    return self._str_repr_()

parameter(value, dt=None) classmethod

Create a Value representing a known instrument parameter (ObjectType.parameter).

Unlike Value.id(), the ObjectType.parameter flag is set immediately so that format(..., 'p') emits the _instrument_var._parameters. prefix without waiting for verify_parameters() to be called.

Parameters:

Name Type Description Default
value str

the parameter name

required
dt 'DataType | None'

optional DataType hint (default: DataType.undefined)

None
Source code in src/mccode_antlr/common/expression/nodes.py
@classmethod
def parameter(cls, value: str, dt: 'DataType | None' = None) -> 'Value':
    """Create a Value representing a known instrument parameter (ObjectType.parameter).

    Unlike Value.id(), the ObjectType.parameter flag is set immediately so that
    format(..., 'p') emits the _instrument_var._parameters. prefix without waiting
    for verify_parameters() to be called.

    Args:
        value: the parameter name
        dt: optional DataType hint (default: DataType.undefined)
    """
    return cls(value, dt if dt is not None else DataType.undefined, ObjectType.parameter, ShapeType.scalar)

eq(other)

Return a BinaryOp expression node for self == other (never a bool).

Source code in src/mccode_antlr/common/expression/nodes.py
def eq(self, other) -> 'BinaryOp':
    """Return a BinaryOp expression node for `self == other` (never a bool)."""
    other = other if isinstance(other, (Value, Op)) else Value.best(other)
    return BinaryOp(self.data_type, OpStyle.C, '__eq__', [self], [other])

ne(other)

Return a BinaryOp expression node for self != other.

Source code in src/mccode_antlr/common/expression/nodes.py
def ne(self, other) -> 'BinaryOp':
    """Return a BinaryOp expression node for `self != other`."""
    other = other if isinstance(other, (Value, Op)) else Value.best(other)
    return BinaryOp(self.data_type, OpStyle.C, '__neq__', [self], [other])

Data types

mccode_antlr.common.expression.DataType

Bases: Enum

Source code in src/mccode_antlr/common/expression/types.py
class DataType(Enum):
    undefined = 0
    float = 1
    int = 2
    str = 3

    def compatible(self, other):
        if self == DataType.undefined or other == DataType.undefined or self == other:
            return True
        if (self == DataType.float and other == DataType.int) or (self == DataType.int and other == DataType.float):
            return True
        return False

    # promotion rules:
    def __add__(self, other):
        if self == DataType.undefined:
            return other
        if other == DataType.undefined:
            return self
        if self == other:
            return self
        if (self == DataType.float and other == DataType.int) or (self == DataType.int and other == DataType.float):
            return DataType.float
        return DataType.str

    __sub__ = __add__
    __mul__ = __add__

    def __truediv__(self, other):
        if self == DataType.str or other == DataType.str:
            raise RuntimeError('Division of strings is undefined')
        return DataType.float

    def __floordiv__(self, other):
        return DataType.int

    @classmethod
    def from_name(cls, name):
        if 'double' in name or 'float' in name:
            return cls.float
        if 'int' in name:
            return cls.int
        if 'char' in name or 'string' in name or 'str' in name:
            return cls.str
        return cls.undefined

    @property
    def name(self):
        if self == DataType.int:
            return 'int'
        if self == DataType.float:
            return 'float'
        if self == DataType.str:
            return 'str'
        return 'undefined'

    @property
    def is_int(self):
        return self == DataType.int

    @property
    def is_float(self):
        return self == DataType.float

    @property
    def is_str(self):
        return self == DataType.str

    @property
    def mccode_c_type(self):
        if self == DataType.float:
            return "double"
        if self == DataType.int:
            return "int"
        if self == DataType.str:
            return "char *"
        raise RuntimeError(f"No known conversion from non-enumerated data type {self}")

mccode_antlr.common.expression.ShapeType

Bases: Enum

Source code in src/mccode_antlr/common/expression/types.py
class ShapeType(Enum):
    unknown = 0
    scalar = 1
    vector = 2

    @property
    def mccode_c_type(self):
        return '*' if self.is_vector else ''

    def compatible(self, other):
        return self == ShapeType.unknown or other == ShapeType.unknown or self == other

    @property
    def is_scalar(self):
        return self == ShapeType.scalar

    @property
    def is_vector(self):
        return self == ShapeType.vector

    def __str__(self):
        return self.name

    @staticmethod
    def from_name(name):
        if 'vector' in name:
            return ShapeType.vector
        if 'scalar' in name:
            return ShapeType.scalar
        return ShapeType.unknown

mccode_antlr.common.expression.ObjectType

Bases: Enum

Source code in src/mccode_antlr/common/expression/types.py
class ObjectType(Enum):
    value = 1
    initializer_list = 2
    identifier = 3
    function = 4
    parameter = 5

    def __str__(self):
        return self.name

    @staticmethod
    def from_name(name):
        if 'value' in name:
            return ObjectType.value
        if 'initializer_list' in name:
            return ObjectType.initializer_list
        if 'identifier' in name:
            return ObjectType.identifier
        if 'function' in name:
            return ObjectType.function
        if 'parameter' in name:
            return ObjectType.parameter
        raise RuntimeError(f"No known conversion from non-enumerated object type {name}")

    @property
    def is_id(self):
        return self == ObjectType.identifier

    @property
    def is_parameter(self):
        return self == ObjectType.parameter

    @property
    def is_function(self):
        return self == ObjectType.function