Repository: Pyomo/pyomo

Issue:
NameError in nl writer
## Summary

NL writer gives a confusing NameError `name 'InvalidNumber' is not defined` when writing an nl file with a variable fixed to None. Seems to be a missing import of `InvalidNumber` or something.

### Steps to reproduce the issue

```python
import pyomo.environ as pyo
m = pyo.ConcreteModel()
m.x = pyo.Var([1, 2, 3, 4])
m.x[1].fix()
m.x[2] = 1
m.x[3] = 2
m.x[4] = 3
m.con = pyo.Constraint(expr= m.x[1] - ((m.x[2]*m.x[3])*m.x[4]) == 0)
m.write("file.nl")
```
This gives:
```
NameError: name 'InvalidNumber' is not defined
```

### Information on your system

Pyomo version: recent main
Python version: 3.8.10


### Additional information

<!-- If you have any additional information, please list it here. -->


<!-- We encourage you to try, as much as possible, to reduce your problem to the minimal example that still reproduces the issue. That would help us a lot in fixing it quickly and effectively!

If you want to ask a question (how to use Pyomo, what it can do, etc.), please refer to our [online documentation](https://github.com/Pyomo/pyomo#getting-help) on ways to get help. -->




Discussion:

Resolving patch:
diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py
--- a/pyomo/repn/linear.py
+++ b/pyomo/repn/linear.py
@@ -42,21 +42,19 @@
 import pyomo.core.kernel as kernel
 from pyomo.repn.util import (
     ExprType,
+    InvalidNumber,
     apply_node_operation,
     complex_number_error,
-    InvalidNumber,
+    nan,
+    sum_like_expression_types,
 )
 
 logger = logging.getLogger(__name__)
 
-nan = float("nan")
-
 _CONSTANT = ExprType.CONSTANT
 _LINEAR = ExprType.LINEAR
 _GENERAL = ExprType.GENERAL
 
-_SumLikeExpression = {SumExpression, LinearExpression, NPV_SumExpression}
-
 
 def _merge_dict(dest_dict, mult, src_dict):
     if mult == 1:
@@ -438,7 +436,7 @@ def _handle_expr_if_const(visitor, node, arg1, arg2, arg3):
     _type, _test = arg1
     assert _type is _CONSTANT
     if _test:
-        if _test != _test:
+        if _test != _test or _test.__class__ is InvalidNumber:
             # nan
             return _handle_expr_if_nonlinear(visitor, node, arg1, arg2, arg3)
         return arg2
@@ -475,7 +473,17 @@ def _handle_expr_if_nonlinear(visitor, node, arg1, arg2, arg3):
 
 
 def _handle_equality_const(visitor, node, arg1, arg2):
-    return _CONSTANT, arg1[1] == arg2[1]
+    # It is exceptionally likely that if we get here, one of the
+    # arguments is an InvalidNumber
+    args, causes = InvalidNumber.parse_args(arg1[1], arg2[1])
+    try:
+        ans = args[0] == args[1]
+    except:
+        ans = False
+        causes.append(str(sys.exc_info()[1]))
+    if causes:
+        ans = InvalidNumber(ans, causes)
+    return _CONSTANT, ans
 
 
 def _handle_equality_general(visitor, node, arg1, arg2):
@@ -495,7 +503,17 @@ def _handle_equality_general(visitor, node, arg1, arg2):
 
 
 def _handle_inequality_const(visitor, node, arg1, arg2):
-    return _CONSTANT, arg1[1] <= arg2[1]
+    # It is exceptionally likely that if we get here, one of the
+    # arguments is an InvalidNumber
+    args, causes = InvalidNumber.parse_args(arg1[1], arg2[1])
+    try:
+        ans = args[0] <= args[1]
+    except:
+        ans = False
+        causes.append(str(sys.exc_info()[1]))
+    if causes:
+        ans = InvalidNumber(ans, causes)
+    return _CONSTANT, ans
 
 
 def _handle_inequality_general(visitor, node, arg1, arg2):
@@ -517,7 +535,17 @@ def _handle_inequality_general(visitor, node, arg1, arg2):
 
 
 def _handle_ranged_const(visitor, node, arg1, arg2, arg3):
-    return _CONSTANT, arg1[1] <= arg2[1] <= arg3[1]
+    # It is exceptionally likely that if we get here, one of the
+    # arguments is an InvalidNumber
+    args, causes = InvalidNumber.parse_args(arg1[1], arg2[1], arg3[1])
+    try:
+        ans = args[0] <= args[1] <= args[2]
+    except:
+        ans = False
+        causes.append(str(sys.exc_info()[1]))
+    if causes:
+        ans = InvalidNumber(ans, causes)
+    return _CONSTANT, ans
 
 
 def _handle_ranged_general(visitor, node, arg1, arg2, arg3):
@@ -548,6 +576,13 @@ def _before_native(visitor, child):
     return False, (_CONSTANT, child)
 
 
+def _before_invalid(visitor, child):
+    return False, (
+        _CONSTANT,
+        InvalidNumber(child, "'{child}' is not a valid numeric type"),
+    )
+
+
 def _before_complex(visitor, child):
     return False, (_CONSTANT, complex_number_error(child, visitor, child))
 
@@ -556,12 +591,7 @@ def _before_var(visitor, child):
     _id = id(child)
     if _id not in visitor.var_map:
         if child.fixed:
-            ans = child()
-            if ans is None or ans != ans:
-                ans = InvalidNumber(nan)
-            elif ans.__class__ in _complex_types:
-                ans = complex_number_error(ans, visitor, child)
-            return False, (_CONSTANT, ans)
+            return False, (_CONSTANT, visitor._eval_fixed(child))
         visitor.var_map[_id] = child
         visitor.var_order[_id] = len(visitor.var_order)
     ans = visitor.Result()
@@ -570,31 +600,13 @@ def _before_var(visitor, child):
 
 
 def _before_param(visitor, child):
-    ans = child()
-    if ans is None or ans != ans:
-        ans = InvalidNumber(nan)
-    elif ans.__class__ in _complex_types:
-        ans = complex_number_error(ans, visitor, child)
-    return False, (_CONSTANT, ans)
+    return False, (_CONSTANT, visitor._eval_fixed(child))
 
 
 def _before_npv(visitor, child):
-    # TBD: It might be more efficient to cache the value of NPV
-    # expressions to avoid duplicate evaluations.  However, current
-    # examples do not benefit from this cache.
-    #
-    # _id = id(child)
-    # if _id in visitor.value_cache:
-    #     child = visitor.value_cache[_id]
-    # else:
-    #     child = visitor.value_cache[_id] = child()
-    # return False, (_CONSTANT, child)
     try:
         return False, (_CONSTANT, visitor._eval_expr(child))
-    except:
-        # If there was an exception evaluating the subexpression, then
-        # we need to descend into it (in case there is something like 0 *
-        # nan that we need to map to 0)
+    except (ValueError, ArithmeticError):
         return True, None
 
 
@@ -605,39 +617,29 @@ def _before_monomial(visitor, child):
     #
     arg1, arg2 = child._args_
     if arg1.__class__ not in native_types:
-        # TBD: It might be more efficient to cache the value of NPV
-        # expressions to avoid duplicate evaluations.  However, current
-        # examples do not benefit from this cache.
-        #
-        # _id = id(arg1)
-        # if _id in visitor.value_cache:
-        #     arg1 = visitor.value_cache[_id]
-        # else:
-        #     arg1 = visitor.value_cache[_id] = arg1()
         try:
             arg1 = visitor._eval_expr(arg1)
-        except:
-            # If there was an exception evaluating the subexpression,
-            # then we need to descend into it (in case there is something
-            # like 0 * nan that we need to map to 0)
+        except (ValueError, ArithmeticError):
             return True, None
 
     # Trap multiplication by 0 and nan.
     if not arg1:
-        if arg2.fixed and arg2.value != arg2.value:
-            deprecation_warning(
-                f"Encountered {arg1}*{str(arg2.value)} in expression tree.  "
-                "Mapping the NaN result to 0 for compatibility "
-                "with the lp_v1 writer.  In the future, this NaN "
-                "will be preserved/emitted to comply with IEEE-754.",
-                version='6.6.0',
-            )
+        if arg2.fixed:
+            arg2 = visitor._eval_fixed(arg2)
+            if arg2 != arg2:
+                deprecation_warning(
+                    f"Encountered {arg1}*{str(arg2.value)} in expression "
+                    "tree.  Mapping the NaN result to 0 for compatibility "
+                    "with the lp_v1 writer.  In the future, this NaN "
+                    "will be preserved/emitted to comply with IEEE-754.",
+                    version='6.6.0',
+                )
         return False, (_CONSTANT, arg1)
 
     _id = id(arg2)
     if _id not in visitor.var_map:
         if arg2.fixed:
-            return False, (_CONSTANT, arg1 * visitor._eval_expr(arg2))
+            return False, (_CONSTANT, arg1 * visitor._eval_fixed(arg2))
         visitor.var_map[_id] = arg2
         visitor.var_order[_id] = len(visitor.var_order)
     ans = visitor.Result()
@@ -658,26 +660,27 @@ def _before_linear(visitor, child):
             if arg1.__class__ not in native_types:
                 try:
                     arg1 = visitor._eval_expr(arg1)
-                except:
-                    # If there was an exception evaluating the
-                    # subexpression, then we need to descend into it (in
-                    # case there is something like 0 * nan that we need
-                    # to map to 0)
+                except (ValueError, ArithmeticError):
                     return True, None
+
+            # Trap multiplication by 0 and nan.
             if not arg1:
-                if arg2.fixed and arg2.value != arg2.value:
-                    deprecation_warning(
-                        f"Encountered {arg1}*{str(arg2.value)} in expression tree.  "
-                        "Mapping the NaN result to 0 for compatibility "
-                        "with the lp_v1 writer.  In the future, this NaN "
-                        "will be preserved/emitted to comply with IEEE-754.",
-                        version='6.6.0',
-                    )
+                if arg2.fixed:
+                    arg2 = visitor._eval_fixed(arg2)
+                    if arg2 != arg2:
+                        deprecation_warning(
+                            f"Encountered {arg1}*{str(arg2.value)} in expression "
+                            "tree.  Mapping the NaN result to 0 for compatibility "
+                            "with the lp_v1 writer.  In the future, this NaN "
+                            "will be preserved/emitted to comply with IEEE-754.",
+                            version='6.6.0',
+                        )
                 continue
+
             _id = id(arg2)
             if _id not in var_map:
                 if arg2.fixed:
-                    const += arg1 * visitor._eval_expr(arg2)
+                    const += arg1 * visitor._eval_fixed(arg2)
                     continue
                 var_map[_id] = arg2
                 var_order[_id] = next_i
@@ -687,17 +690,13 @@ def _before_linear(visitor, child):
                 linear[_id] += arg1
             else:
                 linear[_id] = arg1
-        elif arg.__class__ not in native_numeric_types:
+        elif arg.__class__ in native_numeric_types:
+            const += arg
+        else:
             try:
                 const += visitor._eval_expr(arg)
-            except:
-                # If there was an exception evaluating the
-                # subexpression, then we need to descend into it (in
-                # case there is something like 0 * nan that we need to
-                # map to 0)
+            except (ValueError, ArithmeticError):
                 return True, None
-        else:
-            const += arg
     if linear:
         ans.constant = const
         return False, (_LINEAR, ans)
@@ -717,25 +716,6 @@ def _before_named_expression(visitor, child):
         return True, None
 
 
-def _before_expr_if(visitor, child):
-    test, t, f = child.args
-    if is_fixed(test):
-        try:
-            test = test()
-        except:
-            return True, None
-        subexpr = LinearRepnVisitor(
-            visitor.subexpression_cache, visitor.var_map, visitor.var_order
-        ).walk_expression(t if test else f)
-        if subexpr.nonlinear is not None:
-            return False, (_GENERAL, subexpr)
-        elif subexpr.linear:
-            return False, (_LINEAR, subexpr)
-        else:
-            return False, (_CONSTANT, subexpr.constant)
-    return True, None
-
-
 def _before_external(visitor, child):
     ans = visitor.Result()
     if all(is_fixed(arg) for arg in child.args):
@@ -761,6 +741,8 @@ def _register_new_before_child_dispatcher(visitor, child):
             dispatcher[child_type] = _before_complex
         else:
             dispatcher[child_type] = _before_native
+    elif child_type in native_types:
+        dispatcher[child_type] = _before_invalid
     elif not child.is_expression_type():
         if child.is_potentially_variable():
             dispatcher[child_type] = _before_var
@@ -806,21 +788,10 @@ def _register_new_before_child_dispatcher(visitor, child):
 # complex number types
 _complex_types = set((complex,))
 
-# Register an initial set of known expression types with the "before
-# child" expression handler lookup table.
-for _type in native_numeric_types:
-    _before_child_dispatcher[_type] = _before_native
 # We do not support writing complex numbers out
 _before_child_dispatcher[complex] = _before_complex
-# general operators
-for _type in _exit_node_handlers:
-    _before_child_dispatcher[_type] = _before_general_expression
-# override for named subexpressions
-for _type in _named_subexpression_types:
-    _before_child_dispatcher[_type] = _before_named_expression
-# Special handling for expr_if and external functions: will be handled
+# Special handling for external functions: will be handled
 # as terminal nodes from the point of view of the visitor
-_before_child_dispatcher[Expr_ifExpression] = _before_expr_if
 _before_child_dispatcher[ExternalFunctionExpression] = _before_external
 # Special linear / summation expressions
 _before_child_dispatcher[MonomialTermExpression] = _before_monomial
@@ -857,12 +828,64 @@ def __init__(self, subexpression_cache, var_map, var_order):
         self.var_order = var_order
         self._eval_expr_visitor = _EvaluationVisitor(True)
 
+    def _eval_fixed(self, obj):
+        ans = obj.value
+        if ans.__class__ not in native_numeric_types:
+            # None can be returned from uninitialized Var/Param objects
+            if ans is None:
+                return InvalidNumber(
+                    None, f"'{obj}' contains a nonnumeric value '{ans}'"
+                )
+            if ans.__class__ is InvalidNumber:
+                return ans
+            else:
+                # It is possible to get other non-numeric types.  Most
+                # common are bool and 1-element numpy.array().  We will
+                # attempt to convert the value to a float before
+                # proceeding.
+                #
+                # TODO: we should check bool and warn/error (while bool is
+                # convertible to float in Python, they have very
+                # different semantic meanings in Pyomo).
+                try:
+                    ans = float(ans)
+                except:
+                    return InvalidNumber(
+                        ans, f"'{obj}' contains a nonnumeric value '{ans}'"
+                    )
+        if ans != ans:
+            return InvalidNumber(nan, f"'{obj}' contains a nonnumeric value '{ans}'")
+        if ans.__class__ in _complex_types:
+            return complex_number_error(ans, self, obj)
+        return ans
+
     def _eval_expr(self, expr):
         ans = self._eval_expr_visitor.dfs_postorder_stack(expr)
-        if ans.__class__ not in native_types:
-            ans = value(ans)
+        if ans.__class__ not in native_numeric_types:
+            # None can be returned from uninitialized Expression objects
+            if ans is None:
+                return InvalidNumber(
+                    ans, f"'{expr}' evaluated to nonnumeric value '{ans}'"
+                )
+            if ans.__class__ is InvalidNumber:
+                return ans
+            else:
+                # It is possible to get other non-numeric types.  Most
+                # common are bool and 1-element numpy.array().  We will
+                # attempt to convert the value to a float before
+                # proceeding.
+                #
+                # TODO: we should check bool and warn/error (while bool is
+                # convertible to float in Python, they have very
+                # different semantic meanings in Pyomo).
+                try:
+                    ans = float(ans)
+                except:
+                    return InvalidNumber(
+                        ans, f"'{expr}' evaluated to nonnumeric value '{ans}'"
+                    )
         if ans != ans:
-            return InvalidNumber(ans)
+            return InvalidNumber(ans, f"'{expr}' evaluated to nonnumeric value '{ans}'")
         if ans.__class__ in _complex_types:
             return complex_number_error(ans, self, expr)
         return ans
@@ -879,7 +902,7 @@ def beforeChild(self, node, child, child_idx):
     def enterNode(self, node):
         # SumExpression are potentially large nary operators.  Directly
         # populate the result
-        if node.__class__ in _SumLikeExpression:
+        if node.__class__ in sum_like_expression_types:
             return node.args, self.Result()
         else:
             return node.args, []
diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py
--- a/pyomo/repn/plugins/nl_writer.py
+++ b/pyomo/repn/plugins/nl_writer.py
@@ -11,7 +11,7 @@
 
 import logging
 import os
-from collections import deque
+from collections import deque, defaultdict
 from operator import itemgetter, attrgetter, setitem
 
 from pyomo.common.backports import nullcontext
@@ -72,11 +72,14 @@
     ExprType,
     FileDeterminism,
     FileDeterminism_to_SortComponents,
+    InvalidNumber,
     apply_node_operation,
     categorize_valid_components,
     complex_number_error,
     initialize_var_map_from_column_order,
     ordered_active_constraints,
+    nan,
+    sum_like_expression_types,
 )
 from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env
 
@@ -2233,42 +2236,19 @@ def _before_var(visitor, child):
     _id = id(child)
     if _id not in visitor.var_map:
         if child.fixed:
-            ans = child()
-            if ans is None or ans != ans:
-                ans = InvalidNumber(nan)
-            elif ans.__class__ in _complex_types:
-                ans = complex_number_error(ans, self, node)
-            return False, (_CONSTANT, ans)
+            return False, (_CONSTANT, visitor._eval_fixed(child))
         visitor.var_map[_id] = child
     return False, (_MONOMIAL, _id, 1)
 
 
 def _before_param(visitor, child):
-    ans = child()
-    if ans is None or ans != ans:
-        ans = InvalidNumber(nan)
-    elif ans.__class__ in _complex_types:
-        ans = complex_number_error(ans, self, child)
-    return False, (_CONSTANT, ans)
+    return False, (_CONSTANT, visitor._eval_fixed(child))
 
 
 def _before_npv(visitor, child):
-    # TBD: It might be more efficient to cache the value of NPV
-    # expressions to avoid duplicate evaluations.  However, current
-    # examples do not benefit from this cache.
-    #
-    # _id = id(child)
-    # if _id in visitor.value_cache:
-    #     child = visitor.value_cache[_id]
-    # else:
-    #     child = visitor.value_cache[_id] = child()
-    # return False, (_CONSTANT, child)
     try:
         return False, (_CONSTANT, visitor._eval_expr(child))
-    except:
-        # If there was an exception evaluating the subexpression, then
-        # we need to descend into it (in case there is something like 0 *
-        # nan that we need to map to 0)
+    except (ValueError, ArithmeticError):
         return True, None
 
 
@@ -2279,42 +2259,29 @@ def _before_monomial(visitor, child):
     #
     arg1, arg2 = child._args_
     if arg1.__class__ not in native_types:
-        # TBD: It might be more efficient to cache the value of NPV
-        # expressions to avoid duplicate evaluations.  However, current
-        # examples do not benefit from this cache.
-        #
-        # _id = id(arg1)
-        # if _id in visitor.value_cache:
-        #     arg1 = visitor.value_cache[_id]
-        # else:
-        #     arg1 = visitor.value_cache[_id] = arg1()
         try:
             arg1 = visitor._eval_expr(arg1)
-        except:
-            # If there was an exception evaluating the subexpression,
-            # then we need to descend into it (in case there is something
-            # like 0 * nan that we need to map to 0)
+        except (ValueError, ArithmeticError):
             return True, None
 
-    if arg2.fixed:
-        arg2 = arg2.value
-        _prod = arg1 * arg2
-        if not (arg1 and arg2) and _prod:
-            deprecation_warning(
-                f"Encountered {arg1}*{arg2} in expression tree.  "
-                "Mapping the NaN result to 0 for compatibility "
-                "with the nl_v1 writer.  In the future, this NaN "
-                "will be preserved/emitted to comply with IEEE-754.",
-                version='6.4.3',
-            )
-            _prod = 0
-        return False, (_CONSTANT, _prod)
-
-    # Trap multiplication by 0.
+    # Trap multiplication by 0 and nan.
     if not arg1:
-        return False, (_CONSTANT, 0)
+        if arg2.fixed:
+            arg2 = visitor._eval_fixed(arg2)
+            if arg2 != arg2:
+                deprecation_warning(
+                    f"Encountered {arg1}*{arg2} in expression tree.  "
+                    "Mapping the NaN result to 0 for compatibility "
+                    "with the nl_v1 writer.  In the future, this NaN "
+                    "will be preserved/emitted to comply with IEEE-754.",
+                    version='6.4.3',
+                )
+        return False, (_CONSTANT, arg1)
+
     _id = id(arg2)
     if _id not in visitor.var_map:
+        if arg2.fixed:
+            return False, (_CONSTANT, arg1 * visitor._eval_fixed(arg2))
         visitor.var_map[_id] = arg2
     return False, (_MONOMIAL, _id, arg1)
 
@@ -2328,23 +2295,46 @@ def _before_linear(visitor, child):
     linear = {}
     for arg in child.args:
         if arg.__class__ is MonomialTermExpression:
-            c, v = arg.args
-            if c.__class__ not in native_types:
-                c = visitor._eval_expr(c)
-            if v.fixed:
-                const += c * v.value
-            elif c:
-                _id = id(v)
-                if _id not in var_map:
-                    var_map[_id] = v
-                if _id in linear:
-                    linear[_id] += c
-                else:
-                    linear[_id] = c
+            arg1, arg2 = arg._args_
+            if arg1.__class__ not in native_types:
+                try:
+                    arg1 = visitor._eval_expr(arg1)
+                except (ValueError, ArithmeticError):
+                    return True, None
+
+            # Trap multiplication by 0 and nan.
+            if not arg1:
+                if arg2.fixed:
+                    arg2 = visitor._eval_fixed(arg2)
+                    if arg2 != arg2:
+                        deprecation_warning(
+                            f"Encountered {arg1}*{str(arg2.value)} in expression "
+                            "tree.  Mapping the NaN result to 0 for compatibility "
+                            "with the nl_v1 writer.  In the future, this NaN "
+                            "will be preserved/emitted to comply with IEEE-754.",
+                            version='6.4.3',
+                        )
+                continue
+
+            _id = id(arg2)
+            if _id not in var_map:
+                if arg2.fixed:
+                    const += arg1 * visitor._eval_fixed(arg2)
+                    continue
+                var_map[_id] = arg2
+                linear[_id] = arg1
+            elif _id in linear:
+                linear[_id] += arg1
+            else:
+                linear[_id] = arg1
         elif arg.__class__ in native_types:
             const += arg
         else:
-            const += visitor._eval_expr(arg)
+            try:
+                const += visitor._eval_expr(arg)
+            except (ValueError, ArithmeticError):
+                return True, None
+
     if linear:
         return False, (_GENERAL, AMPLRepn(const, linear, None))
     else:
@@ -2369,28 +2359,55 @@ def _before_general_expression(visitor, child):
     return True, None
 
 
+def _register_new_before_child_handler(visitor, child):
+    handlers = _before_child_handlers
+    child_type = child.__class__
+    if child_type in native_numeric_types:
+        if isinstance(child_type, complex):
+            _complex_types.add(child_type)
+            handlers[child_type] = _before_complex
+        else:
+            handlers[child_type] = _before_native
+    elif issubclass(child_type, str):
+        handlers[child_type] = _before_string
+    elif child_type in native_types:
+        handlers[child_type] = _before_native
+    elif not child.is_expression_type():
+        if child.is_potentially_variable():
+            handlers[child_type] = _before_var
+        else:
+            handlers[child_type] = _before_param
+    elif not child.is_potentially_variable():
+        handlers[child_type] = _before_npv
+        # If we descend into the named expression (because of an
+        # evaluation error), then on the way back out, we will use
+        # the potentially variable handler to process the result.
+        pv_base_type = child.potentially_variable_base_class()
+        if pv_base_type not in handlers:
+            try:
+                child.__class__ = pv_base_type
+                _register_new_before_child_handler(visitor, child)
+            finally:
+                child.__class__ = child_type
+        if pv_base_type in _operator_handles:
+            _operator_handles[child_type] = _operator_handles[pv_base_type]
+    elif id(child) in visitor.subexpression_cache or issubclass(
+        child_type, _GeneralExpressionData
+    ):
+        handlers[child_type] = _before_named_expression
+        _operator_handles[child_type] = handle_named_expression_node
+    else:
+        handlers[child_type] = _before_general_expression
+    return handlers[child_type](visitor, child)
+
+
+_before_child_handlers = defaultdict(lambda: _register_new_before_child_handler)
+
 _complex_types = set((complex,))
-# Register an initial set of known expression types with the "before
-# child" expression handler lookup table.
-_before_child_handlers = {_type: _before_native for _type in native_numeric_types}
 _before_child_handlers[complex] = _before_complex
 for _type in native_types:
     if issubclass(_type, str):
         _before_child_handlers[_type] = _before_string
-# general operators
-for _type in _operator_handles:
-    _before_child_handlers[_type] = _before_general_expression
-# named subexpressions
-for _type in (
-    _GeneralExpressionData,
-    ScalarExpression,
-    kernel.expression.expression,
-    kernel.expression.noclone,
-    _GeneralObjectiveData,
-    ScalarObjective,
-    kernel.objective.objective,
-):
-    _before_child_handlers[_type] = _before_named_expression
 # Special linear / summation expressions
 _before_child_handlers[MonomialTermExpression] = _before_monomial
 _before_child_handlers[LinearExpression] = _before_linear
@@ -2422,10 +2439,64 @@ def __init__(
         self.encountered_string_arguments = False
         self._eval_expr_visitor = _EvaluationVisitor(True)
 
+    def _eval_fixed(self, obj):
+        ans = obj.value
+        if ans.__class__ not in native_numeric_types:
+            # None can be returned from uninitialized Var/Param objects
+            if ans is None:
+                return InvalidNumber(
+                    None, f"'{obj}' contains a nonnumeric value '{ans}'"
+                )
+            if ans.__class__ is InvalidNumber:
+                return ans
+            else:
+                # It is possible to get other non-numeric types.  Most
+                # common are bool and 1-element numpy.array().  We will
+                # attempt to convert the value to a float before
+                # proceeding.
+                #
+                # TODO: we should check bool and warn/error (while bool is
+                # convertible to float in Python, they have very
+                # different semantic meanings in Pyomo).
+                try:
+                    ans = float(ans)
+                except:
+                    return InvalidNumber(
+                        ans, f"'{obj}' contains a  nonnumeric value '{ans}'"
+                    )
+        if ans != ans:
+            return InvalidNumber(nan, f"'{obj}' contains a nonnumeric value '{ans}'")
+        if ans.__class__ in _complex_types:
+            return complex_number_error(ans, self, obj)
+        return ans
+
     def _eval_expr(self, expr):
         ans = self._eval_expr_visitor.dfs_postorder_stack(expr)
-        if ans.__class__ not in native_types:
-            ans = value(ans)
+        if ans.__class__ not in native_numeric_types:
+            # None can be returned from uninitialized Expression objects
+            if ans is None:
+                return InvalidNumber(
+                    ans, f"'{expr}' evaluated to nonnumeric value '{ans}'"
+                )
+            if ans.__class__ is InvalidNumber:
+                return ans
+            else:
+                # It is possible to get other non-numeric types.  Most
+                # common are bool and 1-element numpy.array().  We will
+                # attempt to convert the value to a float before
+                # proceeding.
+                #
+                # TODO: we should check bool and warn/error (while bool is
+                # convertible to float in Python, they have very
+                # different semantic meanings in Pyomo).
+                try:
+                    ans = float(ans)
+                except:
+                    return InvalidNumber(
+                        ans, f"'{expr}' evaluated to nonnumeric value '{ans}'"
+                    )
+        if ans != ans:
+            return InvalidNumber(ans, f"'{expr}' evaluated to nonnumeric value '{ans}'")
         if ans.__class__ in _complex_types:
             return complex_number_error(ans, self, expr)
         return ans
@@ -2439,16 +2510,12 @@ def initializeWalker(self, expr):
         return True, expr
 
     def beforeChild(self, node, child, child_idx):
-        try:
-            return _before_child_handlers[child.__class__](self, child)
-        except KeyError:
-            self._register_new_before_child_processor(child)
         return _before_child_handlers[child.__class__](self, child)
 
     def enterNode(self, node):
         # SumExpression are potentially large nary operators.  Directly
         # populate the result
-        if node.__class__ is SumExpression:
+        if node.__class__ in sum_like_expression_types:
             data = AMPLRepn(0, {}, None)
             data.nonlinear = []
             return node.args, data
@@ -2525,43 +2592,3 @@ def finalizeResult(self, result):
         #
         self.active_expression_source = None
         return ans
-
-    def _register_new_before_child_processor(self, child):
-        handlers = _before_child_handlers
-        child_type = child.__class__
-        if child_type in native_numeric_types:
-            if isinstance(child_type, complex):
-                _complex_types.add(child_type)
-                dispatcher[child_type] = _before_complex
-            else:
-                dispatcher[child_type] = _before_native
-        elif issubclass(child_type, str):
-            handlers[child_type] = _before_string
-        elif child_type in native_types:
-            handlers[child_type] = _before_native
-        elif not child.is_expression_type():
-            if child.is_potentially_variable():
-                handlers[child_type] = _before_var
-            else:
-                handlers[child_type] = _before_param
-        elif not child.is_potentially_variable():
-            handlers[child_type] = _before_npv
-            # If we descend into the named expression (because of an
-            # evaluation error), then on the way back out, we will use
-            # the potentially variable handler to process the result.
-            pv_base_type = child.potentially_variable_base_class()
-            if pv_base_type not in handlers:
-                try:
-                    child.__class__ = pv_base_type
-                    _register_new_before_child_processor(self, child)
-                finally:
-                    child.__class__ = child_type
-            if pv_base_type in _operator_handles:
-                _operator_handles[child_type] = _operator_handles[pv_base_type]
-        elif id(child) in self.subexpression_cache or issubclass(
-            child_type, _GeneralExpressionData
-        ):
-            handlers[child_type] = _before_named_expression
-            _operator_handles[child_type] = handle_named_expression_node
-        else:
-            handlers[child_type] = _before_general_expression
diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py
--- a/pyomo/repn/util.py
+++ b/pyomo/repn/util.py
@@ -12,11 +12,13 @@
 import enum
 import itertools
 import logging
+import operator
 import sys
 
 from pyomo.common.collections import Sequence, ComponentMap
 from pyomo.common.deprecation import deprecation_warning
 from pyomo.common.errors import DeveloperError, InvalidValueError
+from pyomo.core.pyomoobject import PyomoObject
 from pyomo.core.base import (
     Var,
     Param,
@@ -29,12 +31,18 @@
 )
 from pyomo.core.base.component import ActiveComponent
 from pyomo.core.expr.numvalue import native_numeric_types, is_fixed, value
+import pyomo.core.expr as EXPR
 import pyomo.core.kernel as kernel
 
 logger = logging.getLogger('pyomo.core')
 
 valid_expr_ctypes_minlp = {Var, Param, Expression, Objective}
 valid_active_ctypes_minlp = {Block, Constraint, Objective, Suffix}
+sum_like_expression_types = {
+    EXPR.SumExpression,
+    EXPR.LinearExpression,
+    EXPR.NPV_SumExpression,
+}
 
 HALT_ON_EVALUATION_ERROR = False
 nan = float('nan')
@@ -92,45 +100,59 @@ def _missing_(cls, value):
         return super()._missing_(value)
 
 
-class InvalidNumber(object):
-    def __init__(self, value):
+class InvalidNumber(PyomoObject):
+    def __init__(self, value, cause=""):
         self.value = value
-
-    def duplicate(self, new_value):
-        return InvalidNumber(new_value)
-
-    def merge(self, other, new_value):
-        return InvalidNumber(new_value)
+        if cause.__class__ is list:
+            self.causes = list(cause)
+        else:
+            self.causes = [cause]
+
+    @staticmethod
+    def parse_args(*args):
+        causes = []
+        real_args = []
+        for arg in args:
+            if arg.__class__ is InvalidNumber:
+                causes.extend(arg.causes)
+                real_args.append(arg.value)
+            else:
+                real_args.append(arg)
+        return real_args, causes
+
+    def _cmp(self, op, other):
+        args, causes = InvalidNumber.parse_args(self, other)
+        try:
+            return op(*args)
+        except TypeError:
+            # TypeError will be raised when comparing incompatible types
+            # (e.g., int <= None)
+            return False
+
+    def _op(self, op, *args):
+        args, causes = InvalidNumber.parse_args(*args)
+        try:
+            return InvalidNumber(op(*args), causes)
+        except (TypeError, ArithmeticError):
+            # TypeError will be raised when operating on incompatible
+            # types (e.g., int + None); ArithmeticError can be raised by
+            # invalid operations (like divide by zero)
+            return InvalidNumber(self.value, causes)
 
     def __eq__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.value == other.value
-        else:
-            return self.value == other
+        return self._cmp(operator.eq, other)
 
     def __lt__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.value < other.value
-        else:
-            return self.value < other
+        return self._cmp(operator.lt, other)
 
     def __gt__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.value > other.value
-        else:
-            return self.value > other
+        return self._cmp(operator.gt, other)
 
     def __le__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.value <= other.value
-        else:
-            return self.value <= other
+        return self._cmp(operator.le, other)
 
     def __ge__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.value >= other.value
-        else:
-            return self.value >= other
+        return self._cmp(operator.ge, other)
 
     def __str__(self):
         return f'InvalidNumber({self.value})'
@@ -152,85 +174,69 @@ def __format__(self, format_spec):
         # raise InvalidValueError(f'Cannot emit {str(self)} in compiled representation')
 
     def __neg__(self):
-        return self.duplicate(-self.value)
+        return self._op(operator.neg, self)
 
     def __abs__(self):
-        return self.duplicate(abs(self.value))
+        return self._op(operator.abs, self)
 
     def __add__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.merge(other, self.value + other.value)
-        else:
-            return self.duplicate(self.value + other)
+        return self._op(operator.add, self, other)
 
     def __sub__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.merge(other, self.value - other.value)
-        else:
-            return self.duplicate(self.value - other)
+        return self._op(operator.sub, self, other)
 
     def __mul__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.merge(other, self.value * other.value)
-        else:
-            return self.duplicate(self.value * other)
+        return self._op(operator.mul, self, other)
 
     def __truediv__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.merge(other, self.value / other.value)
-        else:
-            return self.duplicate(self.value / other)
+        return self._op(operator.truediv, self, other)
 
     def __pow__(self, other):
-        if other.__class__ is InvalidNumber:
-            return self.merge(other, self.value**other.value)
-        else:
-            return self.duplicate(self.value**other)
+        return self._op(operator.pow, self, other)
 
     def __radd__(self, other):
-        return self.duplicate(other + self.value)
+        return self._op(operator.add, other, self)
 
     def __rsub__(self, other):
-        return self.duplicate(other - self.value)
+        return self._op(operator.sub, other, self)
 
     def __rmul__(self, other):
-        return self.duplicate(other * self.value)
+        return self._op(operator.mul, other, self)
 
     def __rtruediv__(self, other):
-        return self.duplicate(other / self.value)
+        return self._op(operator.truediv, other, self)
 
     def __rpow__(self, other):
-        return self.duplicate(other**self.value)
+        return self._op(operator.pow, other, self)
 
 
 def apply_node_operation(node, args):
     try:
         ans = node._apply_operation(args)
         if ans != ans and ans.__class__ is not InvalidNumber:
-            ans = InvalidNumber(ans)
+            ans = InvalidNumber(ans, "Evaluating '{node}' returned NaN")
         return ans
     except:
+        exc_msg = str(sys.exc_info()[1])
         logger.warning(
             "Exception encountered evaluating expression "
             "'%s(%s)'\n\tmessage: %s\n\texpression: %s"
-            % (node.name, ", ".join(map(str, args)), str(sys.exc_info()[1]), node)
+            % (node.name, ", ".join(map(str, args)), exc_msg, node)
         )
         if HALT_ON_EVALUATION_ERROR:
             raise
-        return InvalidNumber(nan)
+        return InvalidNumber(nan, exc_msg)
 
 
 def complex_number_error(value, visitor, expr, node=""):
     msg = f'Pyomo {visitor.__class__.__name__} does not support complex numbers'
-    logger.warning(
-        ' '.join(filter(None, ("Complex number returned from expression", node)))
-        + f"\n\tmessage: {msg}\n\texpression: {expr}"
-    )
+    cause = ' '.join(filter(None, ("Complex number returned from expression", node)))
+    logger.warning(f"{cause}\n\tmessage: {msg}\n\texpression: {expr}")
     if HALT_ON_EVALUATION_ERROR:
         raise InvalidValueError(
             f'Pyomo {visitor.__class__.__name__} does not support complex numbers'
         )
-    return InvalidNumber(value)
+    return InvalidNumber(value, cause)
 
 
 def categorize_valid_components(


Test patch:
diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py
--- a/pyomo/repn/tests/ampl/test_nlv2.py
+++ b/pyomo/repn/tests/ampl/test_nlv2.py
@@ -18,6 +18,7 @@
 
 import pyomo.repn.util as repn_util
 import pyomo.repn.plugins.nl_writer as nl_writer
+from pyomo.repn.util import InvalidNumber
 from pyomo.repn.tests.nl_diff import nl_diff
 
 from pyomo.common.log import LoggingIntercept
@@ -25,6 +26,7 @@
 from pyomo.core.expr import Expr_if, inequality, LinearExpression
 from pyomo.core.base.expression import ScalarExpression
 from pyomo.environ import (
+    Any,
     ConcreteModel,
     Objective,
     Param,
@@ -64,6 +66,15 @@ def __init__(self, symbolic=False):
             True,
         )
 
+    def __enter__(self):
+        assert nl_writer.AMPLRepn.ActiveVisitor is None
+        nl_writer.AMPLRepn.ActiveVisitor = self.visitor
+        return self
+
+    def __exit__(self, exc_type, exc_value, tb):
+        assert nl_writer.AMPLRepn.ActiveVisitor is self.visitor
+        nl_writer.AMPLRepn.ActiveVisitor = None
+
 
 class Test_AMPLRepnVisitor(unittest.TestCase):
     def test_divide(self):
@@ -454,9 +465,10 @@ def test_errors_unary_func(self):
 
     def test_errors_propagate_nan(self):
         m = ConcreteModel()
-        m.p = Param(mutable=True, initialize=0)
+        m.p = Param(mutable=True, initialize=0, domain=Any)
         m.x = Var()
         m.y = Var()
+        m.z = Var()
         m.y.fix(1)
 
         expr = m.y**2 * m.x**2 * (((3 * m.x) / m.p) * m.x) / m.y
@@ -476,6 +488,61 @@ def test_errors_propagate_nan(self):
         self.assertEqual(repn.linear, {})
         self.assertEqual(repn.nonlinear, None)
 
+        m.y.fix(None)
+        expr = log(m.y) + 3
+        repn = info.visitor.walk_expression((expr, None, None))
+        self.assertEqual(repn.nl, None)
+        self.assertEqual(repn.mult, 1)
+        self.assertEqual(str(repn.const), 'InvalidNumber(nan)')
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
+        expr = 3 * m.y
+        repn = info.visitor.walk_expression((expr, None, None))
+        self.assertEqual(repn.nl, None)
+        self.assertEqual(repn.mult, 1)
+        self.assertEqual(repn.const, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
+        m.p.value = None
+        expr = 5 * (m.p * m.x + 2 * m.z)
+        repn = info.visitor.walk_expression((expr, None, None))
+        self.assertEqual(repn.nl, None)
+        self.assertEqual(repn.mult, 1)
+        self.assertEqual(repn.const, 0)
+        self.assertEqual(repn.linear, {id(m.z): 10, id(m.x): InvalidNumber(None)})
+        self.assertEqual(repn.nonlinear, None)
+
+        expr = m.y * m.x
+        repn = info.visitor.walk_expression((expr, None, None))
+        self.assertEqual(repn.nl, None)
+        self.assertEqual(repn.mult, 1)
+        self.assertEqual(repn.const, 0)
+        self.assertEqual(repn.linear, {id(m.x): InvalidNumber(None)})
+        self.assertEqual(repn.nonlinear, None)
+
+        m.z = Var([1, 2, 3, 4], initialize=lambda m, i: i - 1)
+        m.z[1].fix(None)
+        expr = m.z[1] - ((m.z[2] * m.z[3]) * m.z[4])
+        with INFO() as info:
+            repn = info.visitor.walk_expression((expr, None, None))
+        self.assertEqual(repn.nl, None)
+        self.assertEqual(repn.mult, 1)
+        self.assertEqual(repn.const, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\nv%s\nv%s\nv%s\n')
+        self.assertEqual(repn.nonlinear[1], [id(m.z[2]), id(m.z[3]), id(m.z[4])])
+
+        m.z[3].fix(float('nan'))
+        with INFO() as info:
+            repn = info.visitor.walk_expression((expr, None, None))
+        self.assertEqual(repn.nl, None)
+        self.assertEqual(repn.mult, 1)
+        self.assertEqual(repn.const, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
     def test_linearexpression_npv(self):
         m = ConcreteModel()
         m.x = Var(initialize=4)
diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py
--- a/pyomo/repn/tests/test_linear.py
+++ b/pyomo/repn/tests/test_linear.py
@@ -9,16 +9,28 @@
 #  This software is distributed under the 3-clause BSD License.
 #  ___________________________________________________________________________
 
-from pyomo.common.log import LoggingIntercept
 import pyomo.common.unittest as unittest
 
+from pyomo.common.log import LoggingIntercept
+from pyomo.common.dependencies import numpy, numpy_available
+
 from pyomo.core.expr.compare import assertExpressionsEqual
 from pyomo.core.expr.numeric_expr import LinearExpression, MonomialTermExpression
 from pyomo.core.expr import Expr_if, inequality, LinearExpression, NPV_SumExpression
+import pyomo.repn.linear as linear
 from pyomo.repn.linear import LinearRepn, LinearRepnVisitor
 from pyomo.repn.util import InvalidNumber
 
-from pyomo.environ import ConcreteModel, Param, Var, Expression, ExternalFunction, cos
+from pyomo.environ import (
+    Any,
+    ConcreteModel,
+    Param,
+    Var,
+    Expression,
+    ExternalFunction,
+    cos,
+    log,
+)
 
 nan = float('nan')
 
@@ -125,7 +137,7 @@ def test_scalars(self):
         self.assertEqual(cfg.var_map, {})
         self.assertEqual(cfg.var_order, {})
         self.assertEqual(repn.multiplier, 1)
-        self.assertEqual(str(repn.constant), 'InvalidNumber(nan)')
+        self.assertEqual(repn.constant, InvalidNumber(None))
         self.assertEqual(repn.linear, {})
         self.assertEqual(repn.nonlinear, None)
 
@@ -179,7 +191,7 @@ def test_scalars(self):
         self.assertEqual(cfg.var_map, {})
         self.assertEqual(cfg.var_order, {})
         self.assertEqual(repn.multiplier, 1)
-        self.assertEqual(str(repn.constant), 'InvalidNumber(nan)')
+        self.assertEqual(repn.constant, InvalidNumber(None))
         self.assertEqual(repn.linear, {})
         self.assertEqual(repn.nonlinear, None)
 
@@ -276,6 +288,28 @@ def test_npv(self):
         self.assertEqual(repn.linear, {})
         self.assertEqual(repn.nonlinear, None)
 
+        m.p = None
+
+        cfg = VisitorConfig()
+        repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr)
+        self.assertEqual(cfg.subexpr, {})
+        self.assertEqual(cfg.var_map, {})
+        self.assertEqual(cfg.var_order, {})
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
+        cfg = VisitorConfig()
+        repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr)
+        self.assertEqual(cfg.subexpr, {})
+        self.assertEqual(cfg.var_map, {})
+        self.assertEqual(cfg.var_order, {})
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
     def test_monomial(self):
         m = ConcreteModel()
         m.x = Var()
@@ -399,7 +433,7 @@ def test_monomial(self):
         self.assertEqual(cfg.var_order, {id(m.x): 0})
         self.assertEqual(repn.multiplier, 1)
         self.assertEqual(repn.constant, 0)
-        self.assertStructuredAlmostEqual(repn.linear, {id(m.x): InvalidNumber(nan)})
+        self.assertStructuredAlmostEqual(repn.linear, {id(m.x): InvalidNumber(None)})
         self.assertEqual(repn.nonlinear, None)
 
         m.p.set_value(4)
@@ -485,7 +519,7 @@ def test_monomial(self):
         self.assertEqual(cfg.var_map, {})
         self.assertEqual(cfg.var_order, {})
         self.assertEqual(repn.multiplier, 1)
-        self.assertEqual(str(repn.constant), 'InvalidNumber(nan)')
+        self.assertEqual(repn.constant, InvalidNumber(None))
         self.assertEqual(repn.linear, {})
         self.assertEqual(repn.nonlinear, None)
 
@@ -835,6 +869,32 @@ def test_named_expr(self):
         self.assertEqual(repn.linear, {})
         self.assertEqual(repn.nonlinear, None)
 
+        m.e = None
+
+        cfg = VisitorConfig()
+        repn = LinearRepnVisitor(*cfg).walk_expression(m.e)
+        self.assertEqual(
+            cfg.subexpr, {id(m.e): (linear._CONSTANT, InvalidNumber(None))}
+        )
+        self.assertEqual(cfg.var_map, {})
+        self.assertEqual(cfg.var_order, {})
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
+        cfg = VisitorConfig()
+        repn = LinearRepnVisitor(*cfg).walk_expression(2 * m.e)
+        self.assertEqual(
+            cfg.subexpr, {id(m.e): (linear._CONSTANT, InvalidNumber(None))}
+        )
+        self.assertEqual(cfg.var_map, {})
+        self.assertEqual(cfg.var_order, {})
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
     def test_pow_expr(self):
         m = ConcreteModel()
         m.x = Var()
@@ -1148,7 +1208,11 @@ def test_expr_if(self):
         self.assertEqual(repn.multiplier, 1)
         self.assertEqual(repn.constant, 0)
         self.assertEqual(repn.linear, {})
-        assertExpressionsEqual(self, repn.nonlinear, m.x**2)
+        assertExpressionsEqual(
+            self,
+            repn.nonlinear,
+            Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2),
+        )
 
         cfg = VisitorConfig()
         repn = LinearRepnVisitor(*cfg).walk_expression(f)
@@ -1158,7 +1222,11 @@ def test_expr_if(self):
         self.assertEqual(repn.multiplier, 1)
         self.assertEqual(repn.constant, 0)
         self.assertEqual(repn.linear, {})
-        assertExpressionsEqual(self, repn.nonlinear, m.x**2)
+        assertExpressionsEqual(
+            self,
+            repn.nonlinear,
+            Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2),
+        )
 
         cfg = VisitorConfig()
         repn = LinearRepnVisitor(*cfg).walk_expression(g)
@@ -1168,7 +1236,11 @@ def test_expr_if(self):
         self.assertEqual(repn.multiplier, 1)
         self.assertEqual(repn.constant, 0)
         self.assertEqual(repn.linear, {})
-        assertExpressionsEqual(self, repn.nonlinear, m.x**2)
+        assertExpressionsEqual(
+            self,
+            repn.nonlinear,
+            Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2),
+        )
 
         m.y.unfix()
 
@@ -1210,6 +1282,35 @@ def test_expr_if(self):
             Expr_if(IF=inequality(3, m.y, 5), THEN=m.x, ELSE=m.x**2),
         )
 
+        h = Expr_if(1 / m.y >= 1, m.x, m.x**2)
+
+        cfg = VisitorConfig()
+        repn = LinearRepnVisitor(*cfg).walk_expression(h)
+        self.assertEqual(cfg.subexpr, {})
+        self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+        self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.x): 1})
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, 0)
+        self.assertEqual(repn.linear, {})
+        assertExpressionsEqual(
+            self, repn.nonlinear, Expr_if(IF=1 / m.y >= 1, THEN=m.x, ELSE=m.x**2)
+        )
+
+        m.y.fix(0)
+        cfg = VisitorConfig()
+        repn = LinearRepnVisitor(*cfg).walk_expression(h)
+        self.assertEqual(cfg.subexpr, {})
+        self.assertEqual(cfg.var_map, {id(m.x): m.x})
+        self.assertEqual(cfg.var_order, {id(m.x): 0})
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, 0)
+        self.assertEqual(repn.linear, {})
+        assertExpressionsEqual(
+            self,
+            repn.nonlinear,
+            Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2),
+        )
+
     def test_division(self):
         m = ConcreteModel()
         m.x = Var()
@@ -1312,14 +1413,86 @@ def test_external(self):
         self.assertEqual(repn.linear, {})
         self.assertIs(repn.nonlinear, e)
 
+    def test_errors_propagate_nan(self):
+        m = ConcreteModel()
+        m.p = Param(mutable=True, initialize=0, domain=Any)
+        m.x = Var()
+        m.y = Var()
+        m.z = Var()
+        m.y.fix(1)
+
+        expr = m.y + m.x + m.z + ((3 * m.x) / m.p) / m.y
+        cfg = VisitorConfig()
+        with LoggingIntercept() as LOG:
+            repn = LinearRepnVisitor(*cfg).walk_expression(expr)
+        self.assertEqual(
+            LOG.getvalue(),
+            "Exception encountered evaluating expression 'div(3, 0)'\n"
+            "\tmessage: division by zero\n"
+            "\texpression: 3/p\n",
+        )
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, 1)
+        self.assertEqual(len(repn.linear), 2)
+        self.assertEqual(repn.linear[id(m.z)], 1)
+        self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)')
+        self.assertEqual(repn.nonlinear, None)
+
+        m.y.fix(None)
+        expr = log(m.y) + 3
+        repn = LinearRepnVisitor(*cfg).walk_expression(expr)
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(str(repn.constant), 'InvalidNumber(nan)')
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
+        expr = 3 * m.y
+        repn = LinearRepnVisitor(*cfg).walk_expression(expr)
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
+        m.p.value = None
+        expr = 5 * (m.p * m.x + 2 * m.z)
+        repn = LinearRepnVisitor(*cfg).walk_expression(expr)
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, 0)
+        self.assertEqual(len(repn.linear), 2)
+        self.assertEqual(repn.linear[id(m.z)], 10)
+        self.assertEqual(repn.linear[id(m.x)], InvalidNumber(None))
+        self.assertEqual(repn.nonlinear, None)
+
+        expr = m.y * m.x
+        repn = LinearRepnVisitor(*cfg).walk_expression(expr)
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, 0)
+        self.assertEqual(len(repn.linear), 1)
+        self.assertEqual(repn.linear[id(m.x)], InvalidNumber(None))
+        self.assertEqual(repn.nonlinear, None)
+
+        m.z = Var([1, 2, 3, 4], initialize=lambda m, i: i - 1)
+        m.z[1].fix(None)
+        expr = m.z[1] - ((m.z[2] * m.z[3]) * m.z[4])
+        repn = LinearRepnVisitor(*cfg).walk_expression(expr)
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertIsNotNone(repn.nonlinear)
+
+        m.z[3].fix(float('nan'))
+        repn = LinearRepnVisitor(*cfg).walk_expression(expr)
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, InvalidNumber(None))
+        self.assertEqual(repn.linear, {})
+        self.assertIsNotNone(repn.nonlinear)
+
     def test_type_registrations(self):
         m = ConcreteModel()
 
         cfg = VisitorConfig()
         visitor = LinearRepnVisitor(*cfg)
 
-        import pyomo.repn.linear as linear
-
         _orig_dispatcher = linear._before_child_dispatcher
         linear._before_child_dispatcher = bcd = {}
         try:
@@ -1407,3 +1580,31 @@ def test_to_expression(self):
         expr.linear[id(m.x)] = 0
         expr.linear[id(m.y)] = 0
         assertExpressionsEqual(self, expr.to_expression(visitor), LinearExpression())
+
+    @unittest.skipUnless(numpy_available, "Test requires numpy")
+    def test_nonnumeric(self):
+        m = ConcreteModel()
+        m.p = Param(mutable=True, initialize=numpy.array([3]), domain=Any)
+        m.e = Expression()
+
+        cfg = VisitorConfig()
+        repn = LinearRepnVisitor(*cfg).walk_expression(m.p)
+        self.assertEqual(cfg.subexpr, {})
+        self.assertEqual(cfg.var_map, {})
+        self.assertEqual(cfg.var_order, {})
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(repn.constant, 3)
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)
+
+        m.p = numpy.array([3, 4])
+
+        cfg = VisitorConfig()
+        repn = LinearRepnVisitor(*cfg).walk_expression(m.p)
+        self.assertEqual(cfg.subexpr, {})
+        self.assertEqual(cfg.var_map, {})
+        self.assertEqual(cfg.var_order, {})
+        self.assertEqual(repn.multiplier, 1)
+        self.assertEqual(str(repn.constant), 'InvalidNumber([3 4])')
+        self.assertEqual(repn.linear, {})
+        self.assertEqual(repn.nonlinear, None)