Repository: d0c-s4vage/pfp (python)

Issue:
Boolean logic short circuiting should lazily resolve variable references
## Describe the bug

Short circuiting in 010 editor does not resolve variables until the actual comparison is made. E.g., consider the 010 template below:

```c
local int a = 1;
if (a || DOES_NOT_EXIST) {
    Printf("Short circuit power!\n");
}
```

In 010 Editor, the string `Short circuit power!` is printed.

In pfp, the error `UnresolvedID: Could not resolve field 'DOES_NOT_EXIST'` is raised.

## Stack Trace

```
Traceback (most recent call last):
  File "/usr/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/home/james/__ws__/dev/pfp/pfp/__main__.py", line 61, in <module>
    main(sys.argv)
  File "/home/james/__ws__/dev/pfp/pfp/__main__.py", line 55, in main
    keep_successful=args.keep,
  File "/home/james/__ws__/dev/pfp/pfp/__init__.py", line 91, in parse
    printf=printf,
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 846, in parse
    res = self._run(keep_successful)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 1051, in _run
    traceback,
  File "/home/james/__ws__/dev/pfp/venv3/lib/python3.6/site-packages/six.py", line 695, in reraise
    raise value.with_traceback(tb)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 1024, in _run
    res = self._handle_node(self._ast, None, None, self._stream)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 1133, in _handle_node
    res = self._node_switch[node.__class__](node, scope, ctxt, stream)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 1177, in _handle_file_ast
    self._handle_node(child, scope, ctxt, stream)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 1133, in _handle_node
    res = self._node_switch[node.__class__](node, scope, ctxt, stream)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 2374, in _handle_if
    cond = self._handle_node(node.cond, scope, ctxt, stream)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 1133, in _handle_node
    res = self._node_switch[node.__class__](node, scope, ctxt, stream)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 1878, in _handle_binary_op
    right_val = self._handle_node(node.right, scope, ctxt, stream)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 1133, in _handle_node
    res = self._node_switch[node.__class__](node, scope, ctxt, stream)
  File "/home/james/__ws__/dev/pfp/pfp/interp.py", line 2044, in _handle_id
    raise errors.UnresolvedID(node.coord, node.name)
pfp.errors.PfpError: UnresolvedID: Could not resolve field 'DOES_NOT_EXIST' at /tmp/tmpm_y_oum_:2
Exception at test.bt:2
```

## To Reproduce

Run the provided 010 template

## Expected Behavior

pfp should lazily resolve referenced variables during boolean logic operations

## Implementation/Fix Notes/Thoughts

This could either be really easy, or really hard.


Pull request:
Added better short circuiting to logical or operators
fixes #127 

Patch:
diff --git a/pfp/interp.py b/pfp/interp.py
index 73aafa6..f502497 100644
--- a/pfp/interp.py
+++ b/pfp/interp.py
@@ -1283,6 +1283,8 @@ class PfpInterp(object):
                 #     char a: 8, b:8;
                 #    char c: 8, d:8;
                 if (
+                    isinstance(prev, fields.NumberBase)
+                    and
                     (
                         (
                             self._padded_bitfield
@@ -1873,16 +1875,20 @@ class PfpInterp(object):
             new_left_val._pfp__set_value(left_val)
             left_val = new_left_val
 
-        right_val = self._handle_node(node.right, scope, ctxt, stream)
-        if dest_type is not None and not isinstance(right_val, dest_type):
-            new_right_val = dest_type()
-            new_right_val._pfp__set_value(right_val)
-            right_val = new_right_val
+        # short circuit power!
+        if node.op == "||" and left_val:
+            res = 1
+        else:
+            right_val = self._handle_node(node.right, scope, ctxt, stream)
+            if dest_type is not None and not isinstance(right_val, dest_type):
+                new_right_val = dest_type()
+                new_right_val._pfp__set_value(right_val)
+                right_val = new_right_val
 
-        if node.op not in switch:
-            raise errors.UnsupportedBinaryOperator(node.coord, node.op)
+            if node.op not in switch:
+                raise errors.UnsupportedBinaryOperator(node.coord, node.op)
 
-        res = switch[node.op](left_val, right_val)
+            res = switch[node.op](left_val, right_val)
 
         if type(res) is bool:
             new_res = fields.Int()


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 1739846..352ba33 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -335,6 +335,20 @@ class TestBasic(utils.PfpTestCase):
             stdout="0,1,1,1",
         )
 
+    def test_logical_or_operator_lazy_resolve(self):
+        """Test that logic or operations are lazily resolved
+        """
+        dom = self._test_parse_build(
+            "",
+            """
+                local int a = 1;
+                if (a || DOES_NOT_EXIST) {
+                    Printf("Short circuit power!");
+                }
+            """,
+            stdout="Short circuit power!",
+        )
+
     def test_logical_and_operator(self):
         dom = self._test_parse_build(
             "",