Repository: GridTools/gt4py
Pull request #942 (2022-09-21 12:06:28)

Functional: Fixes for various error source location bugs
## Description

Source locations for error handling had various issues (at least one critical issue). This PR adds better tests and fixes for the most pressing issues. Unfixed issues are considered a separate follow-up task (to avoid scope creep) and will be added to a github issue.

## Requirements

Before submitting this PR, please make sure:

- [x] You have run the code checks, tests and documentation build successfully
- [x] All fixes and all new functionality are tested and documentation is up to date
- [ ] ~~You looked at the [review checklist](https://github.com/GridTools/gt4py/blob/master/REVIEW_CHECKLIST.txt)~~ (file doesn't exist)

Additionally, if this PR contains code authored by new contributors:

- [x] All the authors are covered by a valid contributor assignment agreement,
signed by the employer if needed, provided to ETH Zurich
- [x] The names of all the new contributors have been added to an updated
version of the AUTHORS.rst file included in the PR
 




BenWeber42: Follow up: #943
BenWeber42: It's hard to write good tests for this feature. I considered various ways, but discussions/feedback/suggestions are very much welcome. The same goes for the implementation (I opted for smaller changes that make the tests pass).
egparedes: @BenWeber42 what's the status of this? The PR is useful for fixing bugs in the error reporting messages and the issues mentioned in the review seem easy to fix. Would it be possible to get it merge soon before it goes out of sync?
havogt: I implemented most of the suggested changes, but would like to discuss Rico's remaining comments with someone.
havogt: As discussed, @tehrengruber will implement the remaining changes.

Patch:
diff --git a/.gitpod/.vscode/settings.json b/.gitpod/.vscode/settings.json
index 7a666b7f..1591bdb2 100644
--- a/.gitpod/.vscode/settings.json
+++ b/.gitpod/.vscode/settings.json
@@ -1,5 +1,6 @@
 {
     "python.formatting.provider": "black",
     "python.testing.pytestEnabled": true,
-    "python.defaultInterpreterPath": "/workspace/gt4py/.venv/bin/python"
-}
\ No newline at end of file
+    "python.defaultInterpreterPath": "/workspace/gt4py/.venv/bin/python",
+    "python.terminal.activateEnvironment": true
+}
diff --git a/docs/functional/architecture/0011-On_The_Fly_Compilation.md b/docs/functional/architecture/0011-On_The_Fly_Compilation.md
index 59ab21a0..4bf4f1da 100644
--- a/docs/functional/architecture/0011-On_The_Fly_Compilation.md
+++ b/docs/functional/architecture/0011-On_The_Fly_Compilation.md
@@ -30,7 +30,60 @@ DSL -> IR -> Generate source -> [Bindings -> Write to FS -> Python Module] -> Co
                     GT4Py On-The-Fly Compilation
 ```
 
-The main use case is to execute GT4Py GT4Py programs in a performance portable way from Python. Other use cases may include generating performance portable code to be integrated in external applications, generating code for debugging purposes, pre-compiling libraries of GT4Py programs for use from separate Python applications etc.
+<!--
+@startuml 0011-on-the-fly-overview
+!include <C4/C4_Component>
+
+hide stereotype
+
+AddRelTag('optional', $lineStyle=DashedLine())
+
+Person(user, 'User')
+
+Component(iteratorir, 'Iterator IR', 'IR', 'Canonical representation')
+
+Component_Ext(dslcode, "DSL Code", "FieldView or IteratorView")
+
+Rel_L(user, dslcode, 'supplies (via DSL -> lowering)')
+Rel_D(dslcode, iteratorir, 'is lowered to', 'ffront.decorator.Program.itir, iterator.runtime.FendefDispatcher.itir()')
+
+Component(embedded, "Embedded Execution")
+Rel_L(dslcode, embedded, 'can be directly executed', 'iterator.embedded')
+
+Component_Ext(inputs, 'Program Inputs', 'Fields, Scalars')
+Rel_R(user, inputs, 'supplies')
+Rel(inputs, embedded, 'are passed to')
+
+Container_Boundary(backend, 'Compiled Backend', 'program_processors.runners.gtfn_cpu') {
+
+Container_Boundary(workflow, 'Workflow', 'otf.workflow.Workflow') {
+  Component(program_call, 'Program Call', 'Stage', 'Program + inputs')
+  Rel_R(iteratorir, program_call, 'is added to')
+  Rel(inputs, program_call, 'are added to')
+
+  Component(program_source, 'Program Source', 'Stage', 'Generated source code in any language')
+  Rel_R(program_call, program_source, 'is translated to', 'program_processors.gtfn.gtfn_module')
+
+  Component(bindings_source, 'Bindings Source', 'Stage', 'Bindings for calling Program Source from another language (typically from Python)')
+  Rel_R(program_source, bindings_source, 'is used to generate', 'otf.binding.pybind.bind_source()' , $tags='optional')
+
+  Component(compilable_source, 'Compilable Source', 'Stage', 'Compilable package of Program Source and optionally Bindings Source')
+  Rel(program_source, compilable_source, 'is added to', 'otf.binding.pybind.bind_source()')
+  Rel(bindings_source, compilable_source, 'is added to', 'otf.binding.pybind.bind_source()', $tags='optional')
+
+}
+
+  Component(compiled_program, 'Compiled Program', 'Stage', 'Python callable program')
+  Rel_R(compilable_source, compiled_program, 'is compiled to', 'otf.compilation.compiler.Compiler')
+  Rel(inputs, compiled_program, 'are passed into')
+
+}
+
+@enduml
+-->
+![](_static/0011-on-the-fly-overview.svg)
+
+The main use case is to execute GT4Py programs in a performance portable way from Python. Other use cases may include generating performance portable code to be integrated in external applications, generating code for debugging purposes, pre-compiling libraries of GT4Py programs for use from separate Python applications, etc.
 
 Much of the goals and the resulting architecture comes from lessons learned in GT4Py Cartesian, where originally all the steps of on-the-fly compilation together with code generation were approached as one monolithic implementation per "backend" (method of generating external source files). This had to be refactored to account for some use cases and lead to tight coupling with small changes rippling though more code than necessary.
 
@@ -47,7 +100,7 @@ Backend source code wrapped together with information on how it wants to be call
 **`otf.stages.CompilableSource`:**
 `ProgramSource` wrapped together with optional language bindings (source code that enables the backend source code to be called from another language).
 
-**`otf.stages.CompiledProgram:**
+**`otf.stages.CompiledProgram`:**
 A python object that executes the GT4Py program when called with the same arguments as stored in the `ProgramCall`.
 
 Similarly the steps to go from one of the above stages to another have names.
@@ -66,7 +119,7 @@ As a special case a `BindingStep` for a `ProgramSource`, which does not require
 ## Architecture
 
 The goals of the on-the-fly compilation (OTFC) architecture and design are:
-- guide and inform the design and implementation of future components in this part of the code (code generators, builders, bindings generators, caching strategies etc)
+- guide and inform the design and implementation of future components in this part of the code (code generators, builders, bindings generators, caching strategies, etc)
 - ensure consistency and readability of components
 - keep components easy to reason about
 - keep components (including future ones) intercompatible as much as reasonably possible
diff --git a/docs/functional/architecture/_static/0011-on-the-fly-overview.svg b/docs/functional/architecture/_static/0011-on-the-fly-overview.svg
new file mode 100644
index 00000000..6bfebbe7
--- /dev/null
+++ b/docs/functional/architecture/_static/0011-on-the-fly-overview.svg
@@ -0,0 +1,732 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="827px" preserveAspectRatio="none" style="width:1622px;height:827px;background:#FFFFFF;" version="1.1" viewBox="0 0 1622 827" width="1622px" zoomAndPan="magnify"><defs/><g><!--MD5=[47c14c564e796a575e671c2b0469a7bb]
+cluster backend--><g id="cluster_backend"><rect fill="none" height="494" rx="2.5" ry="2.5" style="stroke:#444444;stroke-width:1.0;stroke-dasharray:7.0,7.0;" width="1346" x="270.5" y="327"/><text fill="#444444" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="152" x="867.5" y="344.4688">Compiled Backend</text><text fill="#444444" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="69" x="909" y="359.4453">[Container]</text></g><!--MD5=[6cebd8022e1b8c9ba6a84a7524f8e7af]
+cluster workflow--><g id="cluster_workflow"><rect fill="none" height="411" rx="2.5" ry="2.5" style="stroke:#444444;stroke-width:1.0;stroke-dasharray:7.0,7.0;" width="1098" x="294.5" y="386"/><text fill="#444444" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="77" x="805" y="403.4688">Workflow</text><text fill="#444444" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="69" x="809" y="418.4453">[Container]</text></g><!--MD5=[768ea89973af6b7c4289680bccdf36de]
+entity compiled_program--><g id="elem_compiled_program"><rect fill="#85BBF0" height="85.9531" rx="2.5" ry="2.5" style="stroke:#78A8D8;stroke-width:0.5;" width="191" x="1409" y="678.5"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="153" x="1428" y="703.9688">Compiled Program</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="39" x="1485" y="718.9453">[Stage]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="1502.5" y="735.0117"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="167" x="1423" y="751.5">Python callable program</text></g><!--MD5=[91d4ab8e59f3d5d3798d66dae5b45386]
+entity program_call--><g id="elem_program_call"><rect fill="#85BBF0" height="85.9531" rx="2.5" ry="2.5" style="stroke:#78A8D8;stroke-width:0.5;" width="145" x="311" y="461.5"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="105" x="331" y="486.9688">Program Call</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="39" x="364" y="501.9453">[Stage]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="381.5" y="518.0117"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="121" x="325" y="534.5">Program + inputs</text></g><!--MD5=[666ea9bf5f2f72457afbd1989afa1ab6]
+entity program_source--><g id="elem_program_source"><rect fill="#85BBF0" height="102.4414" rx="2.5" ry="2.5" style="stroke:#78A8D8;stroke-width:0.5;" width="203" x="721" y="453.5"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="130" x="757.5" y="478.9688">Program Source</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="39" x="803" y="493.9453">[Stage]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="820.5" y="510.0117"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="175" x="735" y="526.5">Generated source code in</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="92" x="776.5" y="542.9883">any language</text></g><!--MD5=[644bafc2ff75c18e9ad08292c433466e]
+entity bindings_source--><g id="elem_bindings_source"><rect fill="#85BBF0" height="135.418" rx="2.5" ry="2.5" style="stroke:#78A8D8;stroke-width:0.5;" width="219" x="1157" y="437"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="133" x="1200" y="462.4688">Bindings Source</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="39" x="1247" y="477.4453">[Stage]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="1264.5" y="493.5117"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="195" x="1171" y="510">Bindings for calling Program</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="142" x="1193.5" y="526.4883">Source from another</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="165" x="1182" y="542.9766">language (typically from</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="52" x="1240.5" y="559.4648">Python)</text></g><!--MD5=[444060c807171c3eaa08985e6d667406]
+entity compilable_source--><g id="elem_compilable_source"><rect fill="#85BBF0" height="118.9297" rx="2.5" ry="2.5" style="stroke:#78A8D8;stroke-width:0.5;" width="202" x="967.5" y="662"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="152" x="992.5" y="687.4688">Compilable Source</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="39" x="1049" y="702.4453">[Stage]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="1066.5" y="718.5117"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="157" x="990" y="735">Compilable package of</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="140" x="996.5" y="751.4883">Program Source and</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="182" x="977.5" y="767.9766">optionally Bindings Source</text></g><!--MD5=[8fc3522a43f8c7199df5e09e5bb0188e]
+entity user--><g id="elem_user"><rect fill="#08427B" height="86.8438" rx="2.5" ry="2.5" style="stroke:#073B6F;stroke-width:0.5;" width="68" x="360.5" y="150"/><image height="48" width="48" x="370.5" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII=" y="160"/><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="37" x="376" y="223.4688">User</text></g><!--MD5=[a8b7d611c732ff8fef7906790d4053a1]
+entity iteratorir--><g id="elem_iteratorir"><rect fill="#85BBF0" height="85.9531" rx="2.5" ry="2.5" style="stroke:#78A8D8;stroke-width:0.5;" width="197" x="7" y="461.5"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="80" x="65.5" y="486.9688">Iterator IR</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="19" x="96" y="501.9453">[IR]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="103.5" y="518.0117"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="173" x="21" y="534.5">Canonical representation</text></g><!--MD5=[120c0689b747e22b28456524aab761f0]
+entity dslcode--><g id="elem_dslcode"><rect fill="#CCCCCC" height="52.9766" rx="2.5" ry="2.5" style="stroke:#BFBFBF;stroke-width:0.5;" width="171" x="20" y="167"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="77" x="67" y="192.4688">DSL Code</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="151" x="30" y="207.4453">[FieldView or IteratorView]</text></g><!--MD5=[72626896c7871fbe974960a26836c8ca]
+entity embedded--><g id="elem_embedded"><rect fill="#85BBF0" height="52.9766" rx="2.5" ry="2.5" style="stroke:#78A8D8;stroke-width:0.5;" width="193" x="252" y="7"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="173" x="262" y="32.4688">Embedded Execution</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="8" x="344.5" y="47.4453">[]</text></g><!--MD5=[0d054bb84ffd728dba12b6dde8f13585]
+entity inputs--><g id="elem_inputs"><rect fill="#CCCCCC" height="52.9766" rx="2.5" ry="2.5" style="stroke:#BFBFBF;stroke-width:0.5;" width="148" x="518.5" y="167"/><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="128" x="528.5" y="192.4688">Program Inputs</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="89" x="548" y="207.4453">[Fields, Scalars]</text></g><!--MD5=[1a0be5c4ef00a64a699e0064c7b45e64]
+reverse link dslcode to user--><g id="link_dslcode_user"><path d="M200.83,193.5 C255.67,193.5 321.25,193.5 360.3,193.5 " fill="none" id="dslcode-backto-user" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="192.94,193.5,200.94,196.5,200.94,190.5,192.94,193.5" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="129" x="209.25" y="173.1016">supplies (via DSL -&gt;</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="58" x="246.75" y="187.2344">lowering)</text></g><!--MD5=[80eead0e05a7cd4a0910a51ddb2c3b15]
+link dslcode to iteratorir--><g id="link_dslcode_iteratorir"><path d="M105.5,220.31 C105.5,271.16 105.5,385.11 105.5,451.93 " fill="none" id="dslcode-to-iteratorir" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="105.5,459.52,108.5,451.52,102.5,451.52,105.5,459.52" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="81" x="179" y="279.6016">is lowered to</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="171" x="134" y="293.7344">[ffront.decorator.Program.itir,</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="226" x="106.5" y="307.8672">iterator.runtime.FendefDispatcher.itir()]</text></g><!--MD5=[e171fe61e0400681f3665224e206e121]
+reverse link embedded to dslcode--><g id="link_embedded_dslcode"><path d="M300.72,65.57 C255.35,95.06 187.94,138.9 145.38,166.57 " fill="none" id="embedded-backto-dslcode" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="307.38,61.24,299.0389,63.0908,302.3126,68.119,307.38,61.24" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="149" x="263.5" y="102.6016">can be directly executed</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="118" x="279" y="116.7344">[iterator.embedded]</text></g><!--MD5=[fd89731658800dc119026ceb8415dd04]
+link user to inputs--><g id="link_user_inputs"><path d="M428.99,193.5 C451.09,193.5 480.78,193.5 508.8,193.5 " fill="none" id="user-to-inputs" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="516.71,193.5,508.71,190.5,508.71,196.5,516.71,193.5" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="53" x="447" y="187.1016">supplies</text></g><!--MD5=[142fa79742f2d38cbe2535e27fa4f992]
+link inputs to embedded--><g id="link_inputs_embedded"><path d="M552.46,166.57 C509.72,138.9 442.03,95.06 396.47,65.57 " fill="none" id="inputs-to-embedded" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="389.79,61.24,394.8677,68.1114,398.1339,63.0783,389.79,61.24" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="84" x="478.5" y="109.6016">are passed to</text></g><!--MD5=[277c08de2bf8d9912734b72f9b281cb6]
+link iteratorir to program_call--><g id="link_iteratorir_program_call"><path d="M204.3,504.5 C236.12,504.5 271.02,504.5 301.49,504.5 " fill="none" id="iteratorir-to-program_call" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="309.29,504.5,301.29,501.5,301.29,507.5,309.29,504.5" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="70" x="222.5" y="498.1016">is added to</text></g><!--MD5=[00c8d146c273e381213c4b8f6700e800]
+link inputs to program_call--><g id="link_inputs_program_call"><path d="M575.04,220.31 C540.37,271.57 462.31,386.98 417.28,453.56 " fill="none" id="inputs-to-program_call" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="413.07,459.78,420.0309,454.8256,415.0568,451.4702,413.07,459.78" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="78" x="542.5" y="293.6016">are added to</text></g><!--MD5=[e016677b499505fa75e24cb7c46dc059]
+link program_call to program_source--><g id="link_program_call_program_source"><path d="M456.48,504.5 C525.97,504.5 631.52,504.5 711.45,504.5 " fill="none" id="program_call-to-program_source" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="719.06,504.5,711.06,501.5,711.06,507.5,719.06,504.5" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="95" x="541" y="484.1016">is translated to</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="228" x="474.5" y="498.2344">[program_processors.gtfn.gtfn_module]</text></g><!--MD5=[38ac55fbee22295d75bfcca046f2d32a]
+link program_source to bindings_source--><g id="link_program_source_bindings_source"><path d="M924.36,504.5 C990.76,504.5 1077.68,504.5 1147.3,504.5 " fill="none" id="program_source-to-bindings_source" style="stroke:#666666;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#666666" points="1155.05,504.5,1147.05,501.5,1147.05,507.5,1155.05,504.5" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="120" x="980.5" y="484.1016">is used to generate</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="196" x="942.5" y="498.2344">[otf.binding.pybind.bind_source()]</text></g><!--MD5=[1e4e37d407d9b017fc8ffb1d6201e470]
+link program_source to compilable_source--><g id="link_program_source_compilable_source"><path d="M843.73,555.96 C856.25,581.17 874.06,610.74 896.5,632 C914.63,649.18 936.74,663.84 958.99,676 " fill="none" id="program_source-to-compilable_source" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="965.85,679.66,960.1982,673.2524,957.3786,678.5486,965.85,679.66" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="70" x="960.5" y="614.6016">is added to</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="196" x="897.5" y="628.7344">[otf.binding.pybind.bind_source()]</text></g><!--MD5=[61baf0f7b25f4a095ccf568ba328442a]
+link bindings_source to compilable_source--><g id="link_bindings_source_compilable_source"><path d="M1156.6,570.26 C1144.59,579.98 1133.21,590.6 1123.5,602 C1110.79,616.91 1100.4,635.24 1092.21,653 " fill="none" id="bindings_source-to-compilable_source" style="stroke:#666666;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#666666" points="1088.97,660.27,1094.9694,654.1866,1089.4901,651.7418,1088.97,660.27" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="70" x="1187.5" y="614.6016">is added to</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="196" x="1124.5" y="628.7344">[otf.binding.pybind.bind_source()]</text></g><!--MD5=[64b90fdc4c35e4684f6e1e2fd011b227]
+link compilable_source to compiled_program--><g id="link_compilable_source_compiled_program"><path d="M1169.9,721.5 C1238.76,721.5 1329.62,721.5 1399.28,721.5 " fill="none" id="compilable_source-to-compiled_program" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="1407.02,721.5,1399.02,718.5,1399.02,724.5,1407.02,721.5" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="89" x="1244.75" y="701.1016">is compiled to</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="203" x="1187.75" y="715.2344">[otf.compilation.compiler.Compiler]</text></g><!--MD5=[039d942a625650c5607abedc36609adb]
+link inputs to compiled_program--><g id="link_inputs_compiled_program"><path d="M666.8,196.09 C844.22,201.42 1285.73,224.11 1392.5,319 C1493.9,409.12 1506.85,582.57 1506.52,668.97 " fill="none" id="inputs-to-compiled_program" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="1506.44,676.79,1509.5113,668.8171,1503.5115,668.7635,1506.44,676.79" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="96" x="1501.5" y="509.1016">are passed into</text></g><!--MD5=[16c3da64a2182d6784f4217e8478ffd2]
+@startuml 0011-on-the-fly-overview
+!include <C4/C4_Component>
+
+hide stereotype
+
+AddRelTag('optional', $lineStyle=DashedLine())
+
+Person(user, 'User')
+
+Component(iteratorir, 'Iterator IR', 'IR', 'Canonical representation')
+
+Component_Ext(dslcode, "DSL Code", "FieldView or IteratorView")
+
+Rel_L(user, dslcode, 'supplies (via DSL -> lowering)')
+Rel_D(dslcode, iteratorir, 'is lowered to', 'ffront.decorator.Program.itir, iterator.runtime.FendefDispatcher.itir()')
+
+Component(embedded, "Embedded Execution")
+Rel_L(dslcode, embedded, 'can be directly executed', 'iterator.embedded')
+
+Component_Ext(inputs, 'Program Inputs', 'Fields, Scalars')
+Rel_R(user, inputs, 'supplies')
+Rel(inputs, embedded, 'are passed to')
+
+Container_Boundary(backend, 'Compiled Backend', 'program_processors.runners.gtfn_cpu') {
+
+Container_Boundary(workflow, 'Workflow', 'otf.workflow.Workflow') {
+  Component(program_call, 'Program Call', 'Stage', 'Program + inputs')
+  Rel_R(iteratorir, program_call, 'is added to')
+  Rel(inputs, program_call, 'are added to')
+
+  Component(program_source, 'Program Source', 'Stage', 'Generated source code in any language')
+  Rel_R(program_call, program_source, 'is translated to', 'program_processors.gtfn.gtfn_module')
+
+  Component(bindings_source, 'Bindings Source', 'Stage', 'Bindings for calling Program Source from another language (typically from Python)')
+  Rel_R(program_source, bindings_source, 'is used to generate', 'otf.binding.pybind.bind_source()' , $tags='optional')
+
+  Component(compilable_source, 'Compilable Source', 'Stage', 'Compilable package of Program Source and optionally Bindings Source')
+  Rel(program_source, compilable_source, 'is added to', 'otf.binding.pybind.bind_source()')
+  Rel(bindings_source, compilable_source, 'is added to', 'otf.binding.pybind.bind_source()', $tags='optional')
+
+}
+
+  Component(compiled_program, 'Compiled Program', 'Stage', 'Python callable program')
+  Rel_R(compilable_source, compiled_program, 'is compiled to', 'otf.compilation.compiler.Compiler')
+  Rel(inputs, compiled_program, 'are passed into')
+
+}
+
+@enduml
+
+@startuml 0011-on-the-fly-overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+skinparam defaultTextAlignment center
+
+skinparam wrapWidth 200
+skinparam maxMessageSize 150
+
+skinparam LegendBorderColor transparent
+skinparam LegendBackgroundColor transparent
+skinparam LegendFontColor #FFFFFF
+
+skinparam shadowing<<legendArea>> false
+skinparam rectangle<<legendArea>> {
+    backgroundcolor #00000000
+    bordercolor #00000000
+}
+
+skinparam rectangle {
+    StereotypeFontSize 12
+    shadowing false
+}
+
+skinparam database {
+    StereotypeFontSize 12
+    shadowing false
+}
+
+skinparam queue {
+    StereotypeFontSize 12
+    shadowing false
+}
+
+skinparam arrow {
+    Color #666666
+    FontColor #666666
+    FontSize 12
+}
+
+skinparam actor {
+    StereotypeFontSize 12
+    shadowing false
+    style awesome
+}
+
+skinparam person {
+    StereotypeFontSize 12
+    shadowing false
+}
+
+skinparam package {
+    StereotypeFontSize 6
+    StereotypeFontColor transparent
+    FontStyle plain
+    BackgroundColor transparent
+}
+
+skinparam rectangle<<boundary>> {
+    Shadowing false
+    StereotypeFontSize 6
+    StereotypeFontColor transparent
+    FontColor #444444
+    BorderColor #444444
+    BackgroundColor transparent
+    BorderStyle dashed
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+skinparam rectangle<<person>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #08427B
+    BorderColor #073B6F
+}
+skinparam database<<person>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #08427B
+    BorderColor #073B6F
+}
+skinparam queue<<person>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #08427B
+    BorderColor #073B6F
+}
+skinparam actor<<person>> {
+    StereotypeFontColor #08427B
+    FontColor #08427B
+    BackgroundColor #08427B
+    BorderColor #073B6F
+}
+skinparam person<<person>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #08427B
+    BorderColor #073B6F
+}
+
+
+skinparam rectangle<<external_person>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #686868
+    BorderColor #8A8A8A
+}
+skinparam database<<external_person>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #686868
+    BorderColor #8A8A8A
+}
+skinparam queue<<external_person>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #686868
+    BorderColor #8A8A8A
+}
+skinparam actor<<external_person>> {
+    StereotypeFontColor #686868
+    FontColor #686868
+    BackgroundColor #686868
+    BorderColor #8A8A8A
+}
+skinparam person<<external_person>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #686868
+    BorderColor #8A8A8A
+}
+
+
+skinparam rectangle<<system>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #1168BD
+    BorderColor #3C7FC0
+}
+skinparam database<<system>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #1168BD
+    BorderColor #3C7FC0
+}
+skinparam queue<<system>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #1168BD
+    BorderColor #3C7FC0
+}
+skinparam actor<<system>> {
+    StereotypeFontColor #1168BD
+    FontColor #1168BD
+    BackgroundColor #1168BD
+    BorderColor #3C7FC0
+}
+skinparam person<<system>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #1168BD
+    BorderColor #3C7FC0
+}
+
+
+skinparam rectangle<<external_system>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #999999
+    BorderColor #8A8A8A
+}
+skinparam database<<external_system>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #999999
+    BorderColor #8A8A8A
+}
+skinparam queue<<external_system>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #999999
+    BorderColor #8A8A8A
+}
+skinparam actor<<external_system>> {
+    StereotypeFontColor #999999
+    FontColor #999999
+    BackgroundColor #999999
+    BorderColor #8A8A8A
+}
+skinparam person<<external_system>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #999999
+    BorderColor #8A8A8A
+}
+
+
+
+
+
+sprite $person [48x48/16] {
+000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000
+0000000000000000000049BCCA7200000000000000000000
+0000000000000000006EFFFFFFFFB3000000000000000000
+00000000000000001CFFFFFFFFFFFF700000000000000000
+0000000000000001EFFFFFFFFFFFFFF80000000000000000
+000000000000000CFFFFFFFFFFFFFFFF6000000000000000
+000000000000007FFFFFFFFFFFFFFFFFF100000000000000
+00000000000001FFFFFFFFFFFFFFFFFFF900000000000000
+00000000000006FFFFFFFFFFFFFFFFFFFF00000000000000
+0000000000000BFFFFFFFFFFFFFFFFFFFF40000000000000
+0000000000000EFFFFFFFFFFFFFFFFFFFF70000000000000
+0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000
+0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000
+0000000000000DFFFFFFFFFFFFFFFFFFFF60000000000000
+0000000000000AFFFFFFFFFFFFFFFFFFFF40000000000000
+00000000000006FFFFFFFFFFFFFFFFFFFE00000000000000
+00000000000000EFFFFFFFFFFFFFFFFFF800000000000000
+000000000000007FFFFFFFFFFFFFFFFFF100000000000000
+000000000000000BFFFFFFFFFFFFFFFF5000000000000000
+0000000000000001DFFFFFFFFFFFFFF70000000000000000
+00000000000000000BFFFFFFFFFFFF500000000000000000
+0000000000000000005DFFFFFFFFA1000000000000000000
+0000000000000000000037ABB96100000000000000000000
+000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000
+000000000000025788300000000005886410000000000000
+000000000007DFFFFFFD9643347BFFFFFFFB400000000000
+0000000004EFFFFFFFFFFFFFFFFFFFFFFFFFFB1000000000
+000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFD200000000
+00000006FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE10000000
+0000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0000000
+000000BFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5000000
+000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD000000
+000009FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF200000
+00000DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF600000
+00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000
+00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA00000
+00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000
+00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000
+00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000
+00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA00000
+00000EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF700000
+000006FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE100000
+0000008FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3000000
+000000014555555555555555555555555555555300000000
+000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000
+}
+
+sprite $person2 [48x48/16] {
+0000000000000000000049BCCA7200000000000000000000
+0000000000000000006EFFFFFFFFB3000000000000000000
+00000000000000001CFFFFFFFFFFFF700000000000000000
+0000000000000001EFFFFFFFFFFFFFF80000000000000000
+000000000000000CFFFFFFFFFFFFFFFF6000000000000000
+000000000000007FFFFFFFFFFFFFFFFFF100000000000000
+00000000000001FFFFFFFFFFFFFFFFFFF900000000000000
+00000000000006FFFFFFFFFFFFFFFFFFFF00000000000000
+0000000000000BFFFFFFFFFFFFFFFFFFFF40000000000000
+0000000000000EFFFFFFFFFFFFFFFFFFFF70000000000000
+0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000
+0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000
+0000000000000DFFFFFFFFFFFFFFFFFFFF60000000000000
+0000000000000AFFFFFFFFFFFFFFFFFFFF40000000000000
+00000000000006FFFFFFFFFFFFFFFFFFFE00000000000000
+00000000000000EFFFFFFFFFFFFFFFFFF800000000000000
+000000000000007FFFFFFFFFFFFFFFFFF100000000000000
+000000000000000BFFFFFFFFFFFFFFFF5000000000000000
+0000000000000001DFFFFFFFFFFFFFF70000000000000000
+00000000000000000BFFFFFFFFFFFF500000000000000000
+0000000000000000005DFFFFFFFFA1000000000000000000
+0000000000000000000037ABB96100000000000000000000
+000000000002578888300000000005888864100000000000
+0000000007DFFFFFFFFD9643347BFFFFFFFFFB4000000000
+00000004EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB10000000
+0000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2000000
+000006FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE100000
+00003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000
+0000BFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50000
+0003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0000
+0009FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2000
+000DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6000
+000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000
+001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000
+001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000
+001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000
+001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA000
+000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000
+000DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6000
+0009FFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFFF2000
+0003FFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFFD0000
+0000BFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFF50000
+00003FFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFB00000
+000006FFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFE100000
+0000007FFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFD2000000
+00000004EFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFB10000000
+0000000007DF8FFFFFFFFFFFFFFFFFFFFFF8FB4000000000
+000000000002578888888888888888888864100000000000
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+skinparam rectangle<<container>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #438DD5
+    BorderColor #3C7FC0
+}
+skinparam database<<container>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #438DD5
+    BorderColor #3C7FC0
+}
+skinparam queue<<container>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #438DD5
+    BorderColor #3C7FC0
+}
+skinparam actor<<container>> {
+    StereotypeFontColor #438DD5
+    FontColor #438DD5
+    BackgroundColor #438DD5
+    BorderColor #3C7FC0
+}
+skinparam person<<container>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #438DD5
+    BorderColor #3C7FC0
+}
+
+
+skinparam rectangle<<external_container>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #B3B3B3
+    BorderColor #A6A6A6
+}
+skinparam database<<external_container>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #B3B3B3
+    BorderColor #A6A6A6
+}
+skinparam queue<<external_container>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #B3B3B3
+    BorderColor #A6A6A6
+}
+skinparam actor<<external_container>> {
+    StereotypeFontColor #B3B3B3
+    FontColor #B3B3B3
+    BackgroundColor #B3B3B3
+    BorderColor #A6A6A6
+}
+skinparam person<<external_container>> {
+    StereotypeFontColor #FFFFFF
+    FontColor #FFFFFF
+    BackgroundColor #B3B3B3
+    BorderColor #A6A6A6
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+skinparam rectangle<<component>> {
+    StereotypeFontColor #000000
+    FontColor #000000
+    BackgroundColor #85BBF0
+    BorderColor #78A8D8
+}
+skinparam database<<component>> {
+    StereotypeFontColor #000000
+    FontColor #000000
+    BackgroundColor #85BBF0
+    BorderColor #78A8D8
+}
+skinparam queue<<component>> {
+    StereotypeFontColor #000000
+    FontColor #000000
+    BackgroundColor #85BBF0
+    BorderColor #78A8D8
+}
+skinparam actor<<component>> {
+    StereotypeFontColor #85BBF0
+    FontColor #85BBF0
+    BackgroundColor #85BBF0
+    BorderColor #78A8D8
+}
+skinparam person<<component>> {
+    StereotypeFontColor #000000
+    FontColor #000000
+    BackgroundColor #85BBF0
+    BorderColor #78A8D8
+}
+
+
+skinparam rectangle<<external_component>> {
+    StereotypeFontColor #000000
+    FontColor #000000
+    BackgroundColor #CCCCCC
+    BorderColor #BFBFBF
+}
+skinparam database<<external_component>> {
+    StereotypeFontColor #000000
+    FontColor #000000
+    BackgroundColor #CCCCCC
+    BorderColor #BFBFBF
+}
+skinparam queue<<external_component>> {
+    StereotypeFontColor #000000
+    FontColor #000000
+    BackgroundColor #CCCCCC
+    BorderColor #BFBFBF
+}
+skinparam actor<<external_component>> {
+    StereotypeFontColor #CCCCCC
+    FontColor #CCCCCC
+    BackgroundColor #CCCCCC
+    BorderColor #BFBFBF
+}
+skinparam person<<external_component>> {
+    StereotypeFontColor #000000
+    FontColor #000000
+    BackgroundColor #CCCCCC
+    BorderColor #BFBFBF
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+hide stereotype
+
+skinparam arrow<<optional>> {
+    Color ;line.dashed
+}
+
+
+rectangle "<$person>\n==User" <<person>> as user 
+
+rectangle "==Iterator IR\n//<size:12>[IR]</size>//\n\n Canonical representation" <<component>> as iteratorir 
+
+rectangle "==DSL Code\n//<size:12>[FieldView or IteratorView]</size>//" <<external_component>> as dslcode 
+
+user -LEFT->> dslcode : **supplies (via DSL -> lowering)**
+dslcode -DOWN->> iteratorir : **is lowered to**\n//<size:12>[ffront.decorator.Program.itir,</size>//\n//<size:12>iterator.runtime.FendefDispatcher.itir()]</size>//
+
+rectangle "==Embedded Execution\n//<size:12>[]</size>//" <<component>> as embedded 
+dslcode -LEFT->> embedded : **can be directly executed**\n//<size:12>[iterator.embedded]</size>//
+
+rectangle "==Program Inputs\n//<size:12>[Fields, Scalars]</size>//" <<external_component>> as inputs 
+user -RIGHT->> inputs : **supplies**
+inputs - ->> embedded : **are passed to**
+
+rectangle "==Compiled Backend\n<size:12>[Container]</size>" <<program_processors.runners.gtfn_cpu>><<boundary>> as backend  {
+
+rectangle "==Workflow\n<size:12>[Container]</size>" <<otf.workflow.Workflow>><<boundary>> as workflow  {
+  rectangle "==Program Call\n//<size:12>[Stage]</size>//\n\n Program + inputs" <<component>> as program_call 
+  iteratorir -RIGHT->> program_call : **is added to**
+  inputs - ->> program_call : **are added to**
+
+  rectangle "==Program Source\n//<size:12>[Stage]</size>//\n\n Generated source code in any language" <<component>> as program_source 
+  program_call -RIGHT->> program_source : **is translated to**\n//<size:12>[program_processors.gtfn.gtfn_module]</size>//
+
+  rectangle "==Bindings Source\n//<size:12>[Stage]</size>//\n\n Bindings for calling Program Source from another language (typically from Python)" <<component>> as bindings_source 
+  program_source -RIGHT->> bindings_source <<optional>> : **is used to generate**\n//<size:12>[otf.binding.pybind.bind_source()]</size>//
+
+  rectangle "==Compilable Source\n//<size:12>[Stage]</size>//\n\n Compilable package of Program Source and optionally Bindings Source" <<component>> as compilable_source 
+  program_source - ->> compilable_source : **is added to**\n//<size:12>[otf.binding.pybind.bind_source()]</size>//
+  bindings_source - ->> compilable_source <<optional>> : **is added to**\n//<size:12>[otf.binding.pybind.bind_source()]</size>//
+
+}
+
+  rectangle "==Compiled Program\n//<size:12>[Stage]</size>//\n\n Python callable program" <<component>> as compiled_program 
+  compilable_source -RIGHT->> compiled_program : **is compiled to**\n//<size:12>[otf.compilation.compiler.Compiler]</size>//
+  inputs - ->> compiled_program : **are passed into**
+
+}
+
+@enduml
+
+PlantUML version 1.2022.12(Sun Oct 23 20:12:26 CEST 2022)
+(GPL source distribution)
+Java Runtime: OpenJDK Runtime Environment
+JVM: OpenJDK 64-Bit Server VM
+Default Encoding: UTF-8
+Language: en
+Country: GB
+--></g></svg>
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index a0070073..5d6f740d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -132,6 +132,7 @@ max-complexity = 15
 doctests = true
 ignore =
     B008    # Do not perform function calls in argument defaults
+    B028    # Consider replacing f"'{foo}'" with f"{foo!r}"  # TODO: review this ignore
     D1      # Public code object needs docstring
     DAR     # Disable dargling errors by default
     E203    # Whitespace before ':' (black formatter breaks this sometimes)
@@ -225,9 +226,6 @@ disallow_incomplete_defs = False
 [mypy-functional.ffront.decorator]
 ignore_errors = True
 
-[mypy-functional.ffront.dialect_parser]
-ignore_errors = True
-
 [mypy-functional.type_system.type_translation]
 ignore_errors = True
 
@@ -237,9 +235,6 @@ ignore_errors = True
 [mypy-functional.iterator.transforms.global_tmps]
 ignore_errors = True
 
-[mypy-functional.iterator.transforms.remap_symbols]
-ignore_errors = True
-
 [mypy-functional.iterator.transforms.popup_tmps]
 ignore_errors = True
 
diff --git a/src/functional/common.py b/src/functional/common.py
index 331eab31..fec59ab1 100644
--- a/src/functional/common.py
+++ b/src/functional/common.py
@@ -17,7 +17,17 @@ from __future__ import annotations
 import enum
 from collections.abc import Sequence
 from dataclasses import dataclass
-from typing import Any, Generic, Optional, Protocol, TypeVar, runtime_checkable
+from typing import (
+    Any,
+    Generic,
+    Optional,
+    Protocol,
+    SupportsFloat,
+    SupportsInt,
+    TypeAlias,
+    TypeVar,
+    runtime_checkable,
+)
 
 import numpy as np
 
@@ -28,6 +38,8 @@ DimT = TypeVar("DimT", bound="Dimension")
 DimsT = TypeVar("DimsT", bound=Sequence["Dimension"])
 DT = TypeVar("DT", bound="DType")
 
+Scalar: TypeAlias = SupportsInt | SupportsFloat | np.int32 | np.int64 | np.float32 | np.float64
+
 
 @enum.unique
 class DimensionKind(StrEnum):
diff --git a/src/functional/ffront/ast_passes/remove_docstrings.py b/src/functional/ffront/ast_passes/remove_docstrings.py
index cb0c652d..f4d4a784 100644
--- a/src/functional/ffront/ast_passes/remove_docstrings.py
+++ b/src/functional/ffront/ast_passes/remove_docstrings.py
@@ -48,7 +48,7 @@ class RemoveDocstrings(ast.NodeTransformer):
     """
 
     @classmethod
-    def apply(cls, node):
+    def apply(cls, node: ast.AST) -> ast.AST:
         return cls().visit(node)
 
     def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
diff --git a/src/functional/ffront/decorator.py b/src/functional/ffront/decorator.py
index 790760de..8d2c4551 100644
--- a/src/functional/ffront/decorator.py
+++ b/src/functional/ffront/decorator.py
@@ -26,14 +26,13 @@ import types
 import typing
 import warnings
 from collections.abc import Callable, Iterable
-from typing import Generator, Generic, SupportsFloat, SupportsInt, TypeAlias, TypeVar
+from typing import Generator, Generic, TypeVar
 
-import numpy as np
 from devtools import debug
 
 from eve.extended_typing import Any, Optional
 from eve.utils import UIDGenerator
-from functional.common import DimensionKind, GridType, GTTypeError
+from functional.common import DimensionKind, GridType, GTTypeError, Scalar
 from functional.ffront import (
     dialect_ast_enums,
     field_operator_ast as foast,
@@ -59,8 +58,6 @@ from functional.program_processors import processor_interface as ppi
 from functional.program_processors.runners import roundtrip
 
 
-Scalar: TypeAlias = SupportsInt | SupportsFloat | np.int32 | np.int64 | np.float32 | np.float64
-
 DEFAULT_BACKEND: Callable = roundtrip.executor
 
 
@@ -166,6 +163,8 @@ def _field_constituents_shape_and_dims(
             yield (arg.shape, dims)
         else:
             yield (None, dims)
+    elif isinstance(arg_type, ts.ScalarType):
+        yield (None, [])
     else:
         raise ValueError("Expected `FieldType` or `TupleType` thereof.")
 
diff --git a/src/functional/ffront/dialect_parser.py b/src/functional/ffront/dialect_parser.py
index a443c322..9dbe248c 100644
--- a/src/functional/ffront/dialect_parser.py
+++ b/src/functional/ffront/dialect_parser.py
@@ -57,10 +57,12 @@ class DialectSyntaxError(common.GTSyntaxError):
         return cls(
             msg,
             lineno=node.lineno,
-            offset=node.col_offset,
+            offset=node.col_offset + 1,  # offset is 1-based for syntax errors
             filename=filename,
             end_lineno=getattr(node, "end_lineno", None),
-            end_offset=getattr(node, "end_col_offset", None),
+            end_offset=(node.end_col_offset + 1)
+            if hasattr(node, "end_col_offset") and node.end_col_offset is not None
+            else None,
             text=text,
         )
 
@@ -77,6 +79,18 @@ class DialectSyntaxError(common.GTSyntaxError):
         )
 
 
+def _ensure_syntax_error_invariants(err: SyntaxError):
+    """Ensure syntax error invariants required to print meaningful error messages."""
+    # If offsets are provided so must line numbers. For example `err.offset` determines
+    # if carets (`^^^^`) are printed below `err.text`. They would be misleading if we
+    # don't know on which line the error occurs.
+    assert err.lineno or not err.offset
+    assert err.end_lineno or not err.end_offset
+    # If the ends are provided so must starts.
+    assert err.lineno or not err.end_lineno
+    assert err.offset or not err.end_offset
+
+
 @dataclass(frozen=True, kw_only=True)
 class DialectParser(ast.NodeVisitor, Generic[DialectRootT]):
     source_definition: SourceDefinition
@@ -94,10 +108,14 @@ class DialectParser(ast.NodeVisitor, Generic[DialectRootT]):
 
         source, filename, starting_line = source_definition
         try:
+            line_offset = starting_line - 1
+            definition_ast: ast.AST
             definition_ast = ast.parse(textwrap.dedent(source)).body[0]
+            definition_ast = ast.increment_lineno(definition_ast, line_offset)
+            line_offset = 0  # line numbers are correct from now on
+
             definition_ast = RemoveDocstrings.apply(definition_ast)
             definition_ast = FixMissingLocations.apply(definition_ast)
-            definition_ast = ast.increment_lineno(definition_ast, starting_line - 1)
             output_ast = cls._postprocess_dialect_ast(
                 cls(
                     source_definition=source_definition,
@@ -108,14 +126,31 @@ class DialectParser(ast.NodeVisitor, Generic[DialectRootT]):
                 annotations,
             )
         except SyntaxError as err:
+            _ensure_syntax_error_invariants(err)
+
             # The ast nodes do not contain information about the path of the
             #  source file or its contents. We add this information here so
             #  that raising an error using :func:`DialectSyntaxError.from_AST`
             #  does not require passing the information on every invocation.
             if not err.filename:
                 err.filename = filename
+
+            # ensure line numbers are relative to the file (respects `starting_line`)
+            if err.lineno:
+                err.lineno = err.lineno + line_offset
+            if err.end_lineno:
+                err.end_lineno = err.end_lineno + line_offset
+
             if not err.text:
-                err.text = source
+                if err.lineno:
+                    source_lineno = err.lineno - starting_line
+                    source_end_lineno = (
+                        (err.end_lineno - starting_line) if err.end_lineno else source_lineno
+                    )
+                    err.text = "\n".join(source.splitlines()[source_lineno : source_end_lineno + 1])
+                else:
+                    err.text = source
+
             raise err
 
         return output_ast
diff --git a/src/functional/ffront/field_operator_ast.py b/src/functional/ffront/field_operator_ast.py
index b1873094..59278b68 100644
--- a/src/functional/ffront/field_operator_ast.py
+++ b/src/functional/ffront/field_operator_ast.py
@@ -11,6 +11,9 @@
 # distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
 #
 # SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
 from typing import Any, Generic, TypeVar, Union
 
 from eve import Coerced, Node, SourceLocation, SymbolName, SymbolRef
@@ -171,10 +174,14 @@ class Return(Stmt):
     value: Expr
 
 
+class BlockStmt(Stmt, SymbolTableTrait):
+    stmts: list[Stmt]
+
+
 class FunctionDefinition(LocatedNode, SymbolTableTrait):
     id: Coerced[SymbolName]  # noqa: A003  # shadowing a python builtin
     params: list[DataSymbol]
-    body: list[Stmt]
+    body: BlockStmt
     closure_vars: list[Symbol]
     type: Union[ts.FunctionType, ts.DeferredType] = ts.DeferredType(  # noqa: A003
         constraint=ts.FunctionType
diff --git a/src/functional/ffront/foast_introspection.py b/src/functional/ffront/foast_introspection.py
new file mode 100644
index 00000000..00b34c60
--- /dev/null
+++ b/src/functional/ffront/foast_introspection.py
@@ -0,0 +1,53 @@
+# GT4Py Project - GridTools Framework
+#
+# Copyright (c) 2014-2023, ETH Zurich
+# All rights reserved.
+#
+# This file is part of the GT4Py project and the GridTools framework.
+# GT4Py is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or any later
+# version. See the LICENSE.txt file at the top-level directory of this
+# distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+import enum
+
+from functional.ffront import field_operator_ast as foast
+
+
+class StmtReturnKind(enum.IntEnum):
+    UNCONDITIONAL_RETURN = 0
+    NO_RETURN = 2
+
+
+def deduce_stmt_return_kind(node: foast.Stmt) -> StmtReturnKind:
+    """
+    Deduce if a statement returns and if so, whether it does unconditionally.
+
+    Example with ``StmtReturnKind.UNCONDITIONAL_RETURN``::
+
+        if cond:
+          return 1
+        else:
+          return 2
+
+    Example with ``StmtReturnKind.NO_RETURN``::
+
+        if cond:
+          result = 1
+        else:
+          result = 2
+    """
+    if isinstance(node, foast.Return):
+        return StmtReturnKind.UNCONDITIONAL_RETURN
+    elif isinstance(node, foast.BlockStmt):
+        for stmt in node.stmts:
+            return_kind = deduce_stmt_return_kind(stmt)
+            if return_kind != StmtReturnKind.NO_RETURN:
+                return return_kind
+        return StmtReturnKind.NO_RETURN
+    elif isinstance(node, (foast.Assign, foast.TupleTargetAssign)):
+        return StmtReturnKind.NO_RETURN
+    else:
+        raise AssertionError(f"Statements of type `{type(node).__name__}` not understood.")
diff --git a/src/functional/ffront/foast_passes/iterable_unpack.py b/src/functional/ffront/foast_passes/iterable_unpack.py
index a45f8ccd..135d5fdd 100644
--- a/src/functional/ffront/foast_passes/iterable_unpack.py
+++ b/src/functional/ffront/foast_passes/iterable_unpack.py
@@ -34,20 +34,6 @@ class UnpackedAssignPass(NodeTranslator, traits.VisitorWithSymbolTableTrait):
         node = cls().visit(node)
         return node
 
-    def visit_FunctionDefinition(self, node: foast.FunctionDefinition, **kwargs):
-        new_body = self.visit(node.body, **kwargs)
-        unrolled_body = self._unroll_tuple_target_assign(new_body)
-        assert isinstance(unrolled_body[-1], foast.Return)
-
-        return foast.FunctionDefinition(
-            id=node.id,
-            params=self.visit(node.params, **kwargs),
-            body=unrolled_body,
-            closure_vars=self.visit(node.closure_vars, **kwargs),
-            type=node.type,
-            location=node.location,
-        )
-
     def _unique_tuple_symbol(self, node: foast.TupleTargetAssign) -> foast.Symbol[Any]:
         sym: foast.Symbol = foast.Symbol(
             id=f"__tuple_tmp_{self.unique_tuple_symbol_id}",
@@ -57,18 +43,16 @@ class UnpackedAssignPass(NodeTranslator, traits.VisitorWithSymbolTableTrait):
         self.unique_tuple_symbol_id += 1
         return sym
 
-    def _unroll_tuple_target_assign(
-        self, body: list[foast.LocatedNode]
-    ) -> list[foast.Assign | foast.LocatedNode]:
-        unrolled: list[foast.Assign | foast.LocatedNode] = []
+    def visit_BlockStmt(self, node: foast.BlockStmt, **kwargs) -> foast.BlockStmt:
+        unrolled_stmts: list[foast.Assign | foast.BlockStmt | foast.Return] = []
 
-        for node in body:
-            if isinstance(node, foast.TupleTargetAssign):
-                num_elts, targets = len(node.value.type.types), node.targets  # type: ignore
+        for stmt in node.stmts:
+            if isinstance(stmt, foast.TupleTargetAssign):
+                num_elts, targets = len(stmt.value.type.types), stmt.targets  # type: ignore
                 indices = compute_assign_indices(targets, num_elts)
-                tuple_symbol = self._unique_tuple_symbol(node)
-                unrolled.append(
-                    foast.Assign(target=tuple_symbol, value=node.value, location=node.location)
+                tuple_symbol = self._unique_tuple_symbol(stmt)
+                unrolled_stmts.append(
+                    foast.Assign(target=tuple_symbol, value=stmt.value, location=stmt.location)
                 )
 
                 for (index, subtarget) in zip(indices, targets):
@@ -81,7 +65,7 @@ class UnpackedAssignPass(NodeTranslator, traits.VisitorWithSymbolTableTrait):
                         slice_indices = list(range(lower, upper))
                         tuple_slice = [
                             foast.Subscript(
-                                value=tuple_name, index=i, type=el_type, location=node.location
+                                value=tuple_name, index=i, type=el_type, location=stmt.location
                             )
                             for i in slice_indices
                         ]
@@ -89,23 +73,23 @@ class UnpackedAssignPass(NodeTranslator, traits.VisitorWithSymbolTableTrait):
                         new_tuple = foast.TupleExpr(
                             elts=tuple_slice,
                             type=el_type,
-                            location=node.location,
+                            location=stmt.location,
                         )
                         new_assign = foast.Assign(
                             target=subtarget.id,
                             value=new_tuple,
-                            location=node.location,
+                            location=stmt.location,
                         )
                     else:
                         new_assign = foast.Assign(
                             target=subtarget,
                             value=foast.Subscript(
-                                value=tuple_name, index=index, type=el_type, location=node.location
+                                value=tuple_name, index=index, type=el_type, location=stmt.location
                             ),
-                            location=node.location,
+                            location=stmt.location,
                         )
-                    unrolled.append(new_assign)
+                    unrolled_stmts.append(new_assign)
             else:
-                unrolled.append(node)
+                unrolled_stmts.append(self.generic_visit(stmt, **kwargs))
 
-        return unrolled
+        return foast.BlockStmt(stmts=unrolled_stmts, location=node.location)
diff --git a/src/functional/ffront/foast_passes/type_deduction.py b/src/functional/ffront/foast_passes/type_deduction.py
index 90f9fd69..63ce98bc 100644
--- a/src/functional/ffront/foast_passes/type_deduction.py
+++ b/src/functional/ffront/foast_passes/type_deduction.py
@@ -110,6 +110,20 @@ def promote_to_mask_type(
         return input_type
 
 
+def deduce_stmt_return_type(node: foast.BlockStmt) -> ts.TypeSpec:
+    """Deduce type of value returned inside a block statement."""
+    for stmt in node.stmts:
+        if isinstance(stmt, foast.Return):
+            return stmt.value.type
+
+    # If the node was constructed by the foast parsing we should never get here, but instead
+    # have gotten an error there.
+    raise AssertionError(
+        "Malformed block statement. Expected a return statement in this context, "
+        "but none was found. Please submit a bug report."
+    )
+
+
 class FieldOperatorTypeDeductionCompletnessValidator(NodeVisitor):
     """Validate an FOAST expression is fully typed."""
 
@@ -151,11 +165,11 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
     >>> untyped_fieldop = FieldOperatorParser(
     ...     source_definition=source_definition, closure_vars=closure_vars, annotations=annotations
     ... ).visit(ast.parse(source_definition.source).body[0])
-    >>> untyped_fieldop.body[0].value.type
+    >>> untyped_fieldop.body.stmts[0].value.type
     DeferredType(constraint=None)
 
     >>> typed_fieldop = FieldOperatorTypeDeduction.apply(untyped_fieldop)
-    >>> assert typed_fieldop.body[0].value.type == ts.FieldType(dtype=ts.ScalarType(
+    >>> assert typed_fieldop.body.stmts[0].value.type == ts.FieldType(dtype=ts.ScalarType(
     ...     kind=ts.ScalarKind.FLOAT64), dims=[IDim])
     """
 
@@ -171,8 +185,7 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
         new_params = self.visit(node.params, **kwargs)
         new_body = self.visit(node.body, **kwargs)
         new_closure_vars = self.visit(node.closure_vars, **kwargs)
-        assert isinstance(new_body[-1], foast.Return)
-        return_type = new_body[-1].value.type
+        return_type = deduce_stmt_return_type(new_body)
         if not isinstance(return_type, (ts.DataType, ts.DeferredType, ts.VoidType)):
             raise FieldOperatorTypeDeductionError.from_foast_node(
                 node,
@@ -215,7 +228,7 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
         new_forward = self.visit(node.forward, **kwargs)
         if not new_forward.type.kind == ts.ScalarKind.BOOL:
             raise FieldOperatorTypeDeductionError.from_foast_node(
-                node, msg=f"Argument `forward` to scan operator `{node.id}` must" f"be a boolean."
+                node, msg=f"Argument `forward` to scan operator `{node.id}` must be a boolean."
             )
         new_init = self.visit(node.init, **kwargs)
         if not all(
@@ -246,7 +259,7 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
         symtable = kwargs["symtable"]
         if node.id not in symtable or symtable[node.id].type is None:
             raise FieldOperatorTypeDeductionError.from_foast_node(
-                node, msg=f"Undeclared symbol {node.id}"
+                node, msg=f"Undeclared symbol `{node.id}`."
             )
 
         symbol = symtable[node.id]
diff --git a/src/functional/ffront/foast_pretty_printer.py b/src/functional/ffront/foast_pretty_printer.py
index 027d289e..8a1b2088 100644
--- a/src/functional/ffront/foast_pretty_printer.py
+++ b/src/functional/ffront/foast_pretty_printer.py
@@ -11,6 +11,9 @@ from functional.ffront import dialect_ast_enums, type_specifications as ts
 
 PropertyIdentifier: TypeAlias = Union[type[foast.LocatedNode], tuple[type[foast.LocatedNode], str]]
 
+INDENTATION_PREFIX: Final[str] = "  "
+
+
 # See https://docs.python.org/3/reference/expressions.html#operator-precedence
 # The following list contains all entries from the above link. Operators that
 # are not modeled in FOAST are kept for ease of comparability and future
@@ -139,11 +142,13 @@ class _PrettyPrinter(TemplatedGenerator):
 
     Return = as_fmt("return {value}")
 
+    BlockStmt = as_mako("${'\\n'.join(stmts)}")
+
     FunctionDefinition = as_mako(
         textwrap.dedent(
             """
             def ${id}(${', '.join(params_annotated)})${return_type}:
-              ${'\\n  '.join(body)}
+            ${indented_body}
             """
         ).strip()
     )
@@ -160,7 +165,13 @@ class _PrettyPrinter(TemplatedGenerator):
         return_type = (
             f" -> {node.type.returns}" if not isinstance(node.type, ts.DeferredType) else ""
         )
-        return self.generic_visit(node, params_annotated=params_annotated, return_type=return_type)
+        indented_body = textwrap.indent(self.visit(node.body), INDENTATION_PREFIX)
+        return self.generic_visit(
+            node,
+            indented_body=indented_body,
+            params_annotated=params_annotated,
+            return_type=return_type,
+        )
 
     FieldOperator = as_fmt("@field_operator\n{definition}")
 
diff --git a/src/functional/ffront/foast_to_itir.py b/src/functional/ffront/foast_to_itir.py
index 73491be0..92141940 100644
--- a/src/functional/ffront/foast_to_itir.py
+++ b/src/functional/ffront/foast_to_itir.py
@@ -15,7 +15,7 @@
 import enum
 import itertools
 from dataclasses import dataclass, field
-from typing import Any, Callable, Optional, cast
+from typing import Any, Callable
 
 import numpy as np
 
@@ -115,7 +115,7 @@ def to_value(node: foast.Expr) -> Callable[[itir.Expr], itir.Expr]:
     ...    return a, b
 
     >>> parsed = FieldOperatorParser.apply_to_function(foo)
-    >>> field_a, scalar_b = parsed.body[-1].value.elts
+    >>> field_a, scalar_b = parsed.body.stmts[-1].value.elts
     >>> to_value(field_a)(im.ref("a"))
     FunCall(fun=SymRef(id=SymbolRef('deref')), args=[SymRef(id=SymbolRef('a'))])
     >>> to_value(scalar_b)(im.ref("a"))
@@ -175,9 +175,13 @@ class FieldOperatorLowering(NodeTranslator):
         #  rest of the lowering inside the body. See ADR-0002 for more details.
         new_body = func_definition.expr
         for i, param in enumerate(func_definition.params):
-            if isinstance(node.definition.params[i].type, ts.ScalarType):
+            if iterator_type_kind(node.definition.params[i].type) == ITIRTypeKind.VALUE:
                 new_body = im.let(param.id, im.deref_(param.id))(new_body)
 
+        assert isinstance(node.type, ts.FieldOperatorType)
+        if iterator_type_kind(node.type.definition.returns) == ITIRTypeKind.ITERATOR:
+            new_body = im.deref_(new_body)
+
         return itir.FunctionDefinition(
             id=func_definition.id,
             params=func_definition.params,
@@ -192,7 +196,7 @@ class FieldOperatorLowering(NodeTranslator):
         return itir.FunctionDefinition(
             id=node.id,
             params=params,
-            expr=self._visit_body(node.body, params=params, symtable=symtable),
+            expr=self.visit_BlockStmt(node.body, inner_expr=None, symtable=symtable),
         )
 
     def visit_ScanOperator(self, node: foast.ScanOperator, **kwargs) -> itir.FunctionDefinition:
@@ -218,27 +222,34 @@ class FieldOperatorLowering(NodeTranslator):
             expr=body,
         )
 
-    def _visit_body(
-        self, body: list[foast.Stmt], params: Optional[list[itir.Sym]] = None, **kwargs
-    ) -> itir.FunCall:
-        *assigns, return_stmt = body
-        current_expr = self.visit(return_stmt, **kwargs)
-
-        for assign in reversed(assigns):
-            current_expr = im.let(*self._visit_assign(cast(foast.Assign, assign), **kwargs))(
-                current_expr
-            )
-
-        return current_expr
+    def visit_Stmt(self, node: foast.Stmt, **kwargs):
+        raise AssertionError("Statements must always be visited in the context of a function.")
+
+    def visit_Return(
+        self, node: foast.Return, *, inner_expr: itir.Expr | None, **kwargs
+    ) -> itir.Expr:
+        return self.visit(node.value, **kwargs)
+
+    def visit_BlockStmt(
+        self, node: foast.BlockStmt, *, inner_expr: itir.Expr | None, **kwargs
+    ) -> itir.Expr:
+        for stmt in reversed(node.stmts):
+            inner_expr = self.visit(stmt, inner_expr=inner_expr, **kwargs)
+        assert inner_expr
+        return inner_expr
+
+    def visit_Assign(
+        self, node: foast.Assign, *, inner_expr: itir.Expr | None, **kwargs
+    ) -> itir.Expr:
+        return im.let(self.visit(node.target, **kwargs), self.visit(node.value, **kwargs))(
+            inner_expr
+        )
 
     def _visit_assign(self, node: foast.Assign, **kwargs) -> tuple[itir.Sym, itir.Expr]:
         sym = self.visit(node.target, **kwargs)
         expr = self.visit(node.value, **kwargs)
         return sym, expr
 
-    def visit_Return(self, node: foast.Return, **kwargs) -> itir.Expr:
-        return to_value(node.value)(self.visit(node.value, **kwargs))
-
     def visit_Symbol(self, node: foast.Symbol, **kwargs) -> itir.Sym:
         return im.sym(node.id)
 
@@ -328,7 +339,7 @@ class FieldOperatorLowering(NodeTranslator):
         lowered_false_expr = self.visit(node.false_expr, **kwargs)
 
         return self._lift_if_field(node)(
-            im.call_("if_")(
+            im.if_(
                 lowered_node_cond,
                 to_value(node.true_expr)(lowered_true_expr),
                 to_value(node.false_expr)(lowered_false_expr),
diff --git a/src/functional/ffront/func_to_foast.py b/src/functional/ffront/func_to_foast.py
index 5f08811e..342877ef 100644
--- a/src/functional/ffront/func_to_foast.py
+++ b/src/functional/ffront/func_to_foast.py
@@ -35,6 +35,7 @@ from functional.ffront.ast_passes import (
     UnchainComparesPass,
 )
 from functional.ffront.dialect_parser import DialectParser, DialectSyntaxError
+from functional.ffront.foast_introspection import StmtReturnKind, deduce_stmt_return_kind
 from functional.ffront.foast_passes.closure_var_folding import ClosureVarFolding
 from functional.ffront.foast_passes.closure_var_type_deduction import ClosureVarTypeDeduction
 from functional.ffront.foast_passes.dead_closure_var_elimination import DeadClosureVarElimination
@@ -67,7 +68,7 @@ class FieldOperatorParser(DialectParser[foast.FunctionDefinition]):
     FunctionDefinition(..., id=SymbolName('field_op'), ...)
     >>> foast_tree.params  # doctest: +ELLIPSIS
     [Symbol(..., id=SymbolName('inp'), type=FieldType(...), ...)]
-    >>> foast_tree.body  # doctest: +ELLIPSIS
+    >>> foast_tree.body.stmts  # doctest: +ELLIPSIS
     [Return(..., value=Name(..., id=SymbolRef('inp')))]
 
 
@@ -82,7 +83,7 @@ class FieldOperatorParser(DialectParser[foast.FunctionDefinition]):
     ...     FieldOperatorParser.apply_to_function(wrong_syntax)
     ... except FieldOperatorSyntaxError as err:
     ...     print(f"Error at [{err.lineno}, {err.offset}] in {err.filename})")
-    Error at [2, 4] in ...functional.ffront.func_to_foast.FieldOperatorParser[...]>)
+    Error at [2, 5] in ...functional.ffront.func_to_foast.FieldOperatorParser[...]>)
     """
 
     syntax_error_cls = FieldOperatorSyntaxError
@@ -170,10 +171,17 @@ class FieldOperatorParser(DialectParser[foast.FunctionDefinition]):
                 )
             )
 
+        new_body = self._visit_stmts(node.body, self._make_loc(node), **kwargs)
+
+        if deduce_stmt_return_kind(new_body) != StmtReturnKind.UNCONDITIONAL_RETURN:
+            raise FieldOperatorSyntaxError.from_AST(
+                node, msg="Function must return a value, but no return statement was found."
+            )
+
         return foast.FunctionDefinition(
             id=node.name,
             params=self.visit(node.args, **kwargs),
-            body=self.visit_stmt_list(node.body, **kwargs),
+            body=new_body,
             closure_vars=closure_var_symbols,
             location=self._make_loc(node),
         )
@@ -314,13 +322,6 @@ class FieldOperatorParser(DialectParser[foast.FunctionDefinition]):
             raise FieldOperatorSyntaxError.from_AST(node, msg="Empty return not allowed")
         return foast.Return(value=self.visit(node.value), location=self._make_loc(node))
 
-    def visit_stmt_list(self, nodes: list[ast.stmt], **kwargs) -> list[foast.Expr]:
-        if not isinstance(last_node := nodes[-1], ast.Return):
-            raise FieldOperatorSyntaxError.from_AST(
-                last_node, msg="Field operator must return a field expression on the last line!"
-            )
-        return [self.visit(node, **kwargs) for node in nodes]
-
     def visit_Expr(self, node: ast.Expr) -> foast.Expr:
         return self.visit(node.value)
 
@@ -394,6 +395,14 @@ class FieldOperatorParser(DialectParser[foast.FunctionDefinition]):
             type=ts.DeferredType(constraint=ts.DataType),
         )
 
+    def _visit_stmts(
+        self, stmts: list[ast.stmt], location: eve.SourceLocation, **kwargs
+    ) -> foast.BlockStmt:
+        return foast.BlockStmt(
+            stmts=[self.visit(el, **kwargs) for el in stmts if not isinstance(el, ast.Pass)],
+            location=location,
+        )
+
     def visit_Compare(self, node: ast.Compare, **kwargs) -> foast.Compare:
         if len(node.ops) != 1 or len(node.comparators) != 1:
             raise FieldOperatorSyntaxError.from_AST(
diff --git a/src/functional/ffront/itir_makers.py b/src/functional/ffront/itir_makers.py
index 899432e5..90b0abd7 100644
--- a/src/functional/ffront/itir_makers.py
+++ b/src/functional/ffront/itir_makers.py
@@ -223,6 +223,11 @@ def tuple_get_(tuple_expr, index):
     return call_("tuple_get")(tuple_expr, index)
 
 
+def if_(cond, true_val, false_val):
+    """Create a not_ FunCall, shorthand for ``call("if_")(expr)``."""
+    return call_("if_")(cond, true_val, false_val)
+
+
 def lift_(expr):
     """Create a lift FunCall, shorthand for ``call(call("lift")(expr))``."""
     return call_(call_("lift")(expr))
diff --git a/src/functional/ffront/past_to_itir.py b/src/functional/ffront/past_to_itir.py
index 2bc5d88f..c12d7dc7 100644
--- a/src/functional/ffront/past_to_itir.py
+++ b/src/functional/ffront/past_to_itir.py
@@ -95,7 +95,7 @@ class ProgramLowering(traits.VisitorWithSymbolTableTrait, NodeTranslator):
         """Generate symbols for each field param and dimension."""
         size_params = []
         for param in node.params:
-            if type_info.is_field_type_or_tuple_of_field_type(param.type):
+            if type_info.is_type_or_tuple_of_type(param.type, ts.FieldType):
                 fields_dims: list[list[Dimension]] = (
                     type_info.primitive_constituents(param.type).getattr("dims").to_list()
                 )
@@ -131,7 +131,7 @@ class ProgramLowering(traits.VisitorWithSymbolTableTrait, NodeTranslator):
 
     def _visit_stencil_call(self, node: past.Call, **kwargs) -> itir.StencilClosure:
         assert isinstance(node.kwargs["out"].type, ts.TypeSpec)
-        assert type_info.is_field_type_or_tuple_of_field_type(node.kwargs["out"].type)
+        assert type_info.is_type_or_tuple_of_type(node.kwargs["out"].type, ts.FieldType)
 
         domain = node.kwargs.get("domain", None)
         output, lowered_domain = self._visit_stencil_call_out_arg(
diff --git a/src/functional/ffront/type_info.py b/src/functional/ffront/type_info.py
index 7b5636e5..a617fe67 100644
--- a/src/functional/ffront/type_info.py
+++ b/src/functional/ffront/type_info.py
@@ -28,11 +28,11 @@ from functional.type_system.type_info import (  # noqa: F401
     is_arithmetic as is_arithmetic,
     is_concrete as is_concrete,
     is_concretizable as is_concretizable,
-    is_field_type_or_tuple_of_field_type as is_field_type_or_tuple_of_field_type,
     is_floating_point as is_floating_point,
     is_integral as is_integral,
     is_logical as is_logical,
     is_number as is_number,
+    is_type_or_tuple_of_type as is_type_or_tuple_of_type,
     primitive_constituents as primitive_constituents,
     promote as promote,
     promote_dims as promote_dims,
@@ -52,17 +52,29 @@ def _is_zero_dim_field(field: ts.TypeSpec) -> bool:
 
 
 def promote_zero_dims(
-    args: list[ts.TypeSpec], function_type: ts.FieldOperatorType | ts.ProgramType
+    args: list[ts.TypeSpec], function_type: ts.FieldOperatorType | ts.ProgramType | ts.FunctionType
 ) -> list:
     """Promote arg types to zero dimensional fields if compatible and required by function signature."""
-    new_args = args.copy()
+    new_args = []
     for arg_i, arg in enumerate(args):
-        def_type = function_type.definition.args[arg_i]
-        if _is_zero_dim_field(def_type) and is_number(arg):
-            if extract_dtype(def_type) == extract_dtype(arg):
-                new_args[arg_i] = def_type
-            else:
-                raise GTTypeError(f"{arg} is not compatible with {def_type}.")
+        def_type = (
+            function_type.args[arg_i]
+            if isinstance(function_type, ts.FunctionType)
+            else function_type.definition.args[arg_i]
+        )
+
+        def _as_field(arg: ts.TypeSpec, path: tuple):
+            el_def_type = reduce(lambda type_, idx: type_.types[idx], path, def_type)  # noqa: B023
+
+            if _is_zero_dim_field(el_def_type) and is_number(arg):
+                if extract_dtype(el_def_type) == extract_dtype(arg):
+                    return el_def_type
+                else:
+                    raise GTTypeError(f"{arg} is not compatible with {el_def_type}.")
+            return arg
+
+        new_args.append(apply_to_primitive_constituents(arg, _as_field, with_path_arg=True))
+
     return new_args
 
 
@@ -89,8 +101,8 @@ def function_signature_incompatibilities_fieldop(
 def function_signature_incompatibilities_scanop(
     scanop_type: ts.ScanOperatorType, args: list[ts.TypeSpec], kwargs: dict[str, ts.TypeSpec]
 ) -> Iterator[str]:
-    if not all(is_field_type_or_tuple_of_field_type(arg) for arg in args):
-        yield "Arguments to scan operator must be fields or tuples thereof."
+    if not all(is_type_or_tuple_of_type(arg, (ts.ScalarType, ts.FieldType)) for arg in args):
+        yield "Arguments to scan operator must be fields, scalars or tuples thereof."
         return
 
     arg_dims = [extract_dims(el) for arg in args for el in primitive_constituents(arg)]
@@ -132,7 +144,9 @@ def function_signature_incompatibilities_scanop(
         returns=ts.DeferredType(constraint=None),
     )
 
-    yield from function_signature_incompatibilities(function_type, args, kwargs)
+    yield from function_signature_incompatibilities(
+        function_type, promote_zero_dims(args, function_type), kwargs
+    )
 
 
 @function_signature_incompatibilities.register
diff --git a/src/functional/iterator/embedded.py b/src/functional/iterator/embedded.py
index fd84c6d2..6ccc79a8 100644
--- a/src/functional/iterator/embedded.py
+++ b/src/functional/iterator/embedded.py
@@ -163,6 +163,11 @@ class MutableLocatedField(LocatedField, Protocol):
         ...
 
 
+_column_range: Optional[
+    range
+] = None  # TODO this is a bit ugly, alternative: pass scan range via iterator
+
+
 class Column(np.lib.mixins.NDArrayOperatorsMixin):
     """Represents a column when executed in column mode (`column_axis != None`).
 
@@ -170,13 +175,13 @@ class Column(np.lib.mixins.NDArrayOperatorsMixin):
     and simplify dispatching in iterator ir builtins.
     """
 
-    def __init__(self, kstart: int, data: np.ndarray) -> None:
+    def __init__(self, kstart: int, data: np.ndarray | Scalar) -> None:
         self.kstart = kstart
-        self.data = data
+        assert isinstance(data, (np.ndarray, Scalar))  # type: ignore # mypy bug
+        self.data = data if isinstance(data, np.ndarray) else np.full(len(_column_range), data)  # type: ignore[arg-type]
 
     def __getitem__(self, i: int) -> Any:
         result = self.data[i - self.kstart]
-        # if the element type is a tuple return a regular type instead of a
         #  numpy type
         if self.data.dtype.names:
             return tuple(result)
@@ -702,7 +707,7 @@ def _get_axes(
 
 
 def _make_tuple(
-    field_or_tuple: LocatedField | tuple,  # arbitrary nesting of tuples of LocatedField
+    field_or_tuple: LocatedField | tuple,
     indices: FieldIndexOrIndices,
     *,
     as_column: bool = False,
@@ -999,7 +1004,6 @@ class ScanArgIterator:
     def deref(self) -> Any:
         if not self.can_deref():
             return _UNDEFINED
-        # TODO(tehrengruber): _make_tuple is for fields
         return _make_tuple(self.wrapped_iter.deref(), self.k_pos)
 
     def can_deref(self) -> bool:
@@ -1108,11 +1112,6 @@ def as_tuple_field(field):
     return field
 
 
-_column_range: Optional[
-    range
-] = None  # TODO this is a bit ugly, alternative: pass scan range via iterator
-
-
 def _column_dtype(elem: Any) -> np.dtype:
     if isinstance(elem, tuple):
         return np.dtype([(f"f{i}", _column_dtype(e)) for i, e in enumerate(elem)])
diff --git a/src/functional/iterator/transforms/inline_lifts.py b/src/functional/iterator/transforms/inline_lifts.py
index 8c1e800b..3d457332 100644
--- a/src/functional/iterator/transforms/inline_lifts.py
+++ b/src/functional/iterator/transforms/inline_lifts.py
@@ -170,8 +170,8 @@ class InlineLifts(traits.VisitorWithSymbolTableTrait, NodeTranslator):
             #  lift(λ(a) → inner_ex(shift(...)(a)))(lift(λ(b) → outer_ex(b))(arg))
             # Note: This branch is only needed when there is no outer `deref` by which the previous
             # branches eliminate the lift calls. This occurs for example for the `reduce` builtin
-            # or when a better readible expression of a lift statement is needed during debugging.
-            # Due to it's complexity we might want to remove this branch at some point again,
+            # or when a better readable expression of a lift statement is needed during debugging.
+            # Due to its complexity we might want to remove this branch at some point again,
             # when we see that it is not required.
             stencil = node.fun.args[0]
             eligible_lifted_args = [
diff --git a/src/functional/iterator/transforms/pass_manager.py b/src/functional/iterator/transforms/pass_manager.py
index e464103c..e177608e 100644
--- a/src/functional/iterator/transforms/pass_manager.py
+++ b/src/functional/iterator/transforms/pass_manager.py
@@ -10,6 +10,7 @@ from functional.iterator.transforms.inline_lambdas import InlineLambdas
 from functional.iterator.transforms.inline_lifts import InlineLifts
 from functional.iterator.transforms.merge_let import MergeLet
 from functional.iterator.transforms.normalize_shifts import NormalizeShifts
+from functional.iterator.transforms.propagate_deref import PropagateDeref
 from functional.iterator.transforms.unroll_reduce import UnrollReduce
 
 
@@ -44,6 +45,7 @@ def apply_common_transforms(
     ir = MergeLet().visit(ir)
     ir = InlineFundefs().visit(ir)
     ir = PruneUnreferencedFundefs().visit(ir)
+    ir = PropagateDeref.apply(ir)
     ir = NormalizeShifts().visit(ir)
     if lift_mode != LiftMode.FORCE_TEMPORARIES:
         for _ in range(10):
diff --git a/src/functional/iterator/transforms/propagate_deref.py b/src/functional/iterator/transforms/propagate_deref.py
new file mode 100644
index 00000000..34288757
--- /dev/null
+++ b/src/functional/iterator/transforms/propagate_deref.py
@@ -0,0 +1,45 @@
+from eve import NodeTranslator
+from eve.pattern_matching import ObjectPattern as P
+from functional.iterator import ir
+
+
+# TODO(tehrengruber): This pass can be generalized to all builtins, e.g.
+#  `plus((λ(...) → multiplies(...))(...), ...)` can be transformed into
+#  `(λ(...) → plus(multiplies(...), ...))(...)`.
+
+
+class PropagateDeref(NodeTranslator):
+    @classmethod
+    def apply(cls, node: ir.Node):
+        """
+        Propagate calls to deref into lambda functions.
+
+        Transform::
+
+            ·(λ(inner_it) → lift(stencil)(inner_it))(outer_it)
+
+        into::
+
+            (λ(inner_it) → ·(lift(stencil)(inner_it)))(outer_it)
+
+        After this pass calls to `deref` are grouped together (better) with lambda functions
+        being propagated outwards. This increases the ability of the lift inliner to remove the
+        deref + lift combination.
+        """
+        return cls().visit(node)
+
+    def visit_FunCall(self, node: ir.FunCall):
+        if P(ir.FunCall, fun=ir.SymRef(id="deref"), args=[P(ir.FunCall, fun=P(ir.Lambda))]).match(
+            node
+        ):
+            builtin = node.fun
+            lambda_fun: ir.Lambda = node.args[0].fun  # type: ignore[attr-defined] # invariant ensured by pattern match above
+            lambda_args: list[ir.Expr] = node.args[0].args  # type: ignore[attr-defined] # invariant ensured by pattern match above
+            node = ir.FunCall(
+                fun=ir.Lambda(
+                    params=lambda_fun.params,
+                    expr=ir.FunCall(fun=builtin, args=[lambda_fun.expr]),
+                ),
+                args=lambda_args,
+            )
+        return self.generic_visit(node)
diff --git a/src/functional/iterator/transforms/remap_symbols.py b/src/functional/iterator/transforms/remap_symbols.py
index af11b61b..68ec6660 100644
--- a/src/functional/iterator/transforms/remap_symbols.py
+++ b/src/functional/iterator/transforms/remap_symbols.py
@@ -1,6 +1,6 @@
 from typing import Any, Dict, Optional, Set
 
-from eve import NodeTranslator
+from eve import NodeTranslator, SymbolTableTrait
 from functional.iterator import ir
 
 
@@ -17,7 +17,7 @@ class RemapSymbolRefs(NodeTranslator):
         )
 
     def generic_visit(self, node: ir.Node, **kwargs: Any):  # type: ignore[override]
-        assert isinstance(node, ir.SymbolTableTrait) == isinstance(
+        assert isinstance(node, SymbolTableTrait) == isinstance(
             node, ir.Lambda
         ), "found unexpected new symbol scope"
         return super().generic_visit(node, **kwargs)
@@ -41,7 +41,7 @@ class RenameSymbols(NodeTranslator):
     def generic_visit(  # type: ignore[override]
         self, node: ir.Node, *, name_map: Dict[str, str], active: Optional[Set[str]] = None
     ):
-        if isinstance(node, ir.SymbolTableTrait):
+        if isinstance(node, SymbolTableTrait):
             if active is None:
                 active = set()
             active = active | set(node.annex.symtable)
diff --git a/src/functional/type_system/type_info.py b/src/functional/type_system/type_info.py
index 758fe9bd..1e61acab 100644
--- a/src/functional/type_system/type_info.py
+++ b/src/functional/type_system/type_info.py
@@ -227,22 +227,24 @@ def is_arithmetic(symbol_type: ts.TypeSpec) -> bool:
     return is_floating_point(symbol_type) or is_integral(symbol_type)
 
 
-def is_field_type_or_tuple_of_field_type(type_: ts.TypeSpec) -> bool:
+def is_type_or_tuple_of_type(type_: ts.TypeSpec, expected_type: type | tuple) -> bool:
     """
-     Return True if ``type_`` is FieldType or FieldType nested in TupleType.
+    Return True if ``type_`` matches any of the expected.
 
-     Examples:
-     ---------
+    Examples:
+    ---------
     >>> scalar_type = ts.ScalarType(kind=ts.ScalarKind.INT)
     >>> field_type = ts.FieldType(dims=[], dtype=scalar_type)
-     >>> is_field_type_or_tuple_of_field_type(field_type)
-     True
-     >>> is_field_type_or_tuple_of_field_type(ts.TupleType(types=[field_type, field_type]))
-     True
-     >>> is_field_type_or_tuple_of_field_type(ts.TupleType(types=[field_type, scalar_type]))
-     False
+    >>> is_type_or_tuple_of_type(field_type, ts.FieldType)
+    True
+    >>> is_type_or_tuple_of_type(ts.TupleType(types=[scalar_type, field_type]), (ts.ScalarType, ts.FieldType))
+    True
+    >>> is_type_or_tuple_of_type(scalar_type, ts.FieldType)
+    False
+    >>> is_type_or_tuple_of_type(ts.TupleType(types=[scalar_type, field_type]), ts.FieldType)
+    False
     """
-    return all(isinstance(t, ts.FieldType) for t in primitive_constituents(type_))
+    return all(isinstance(t, expected_type) for t in primitive_constituents(type_))
 
 
 def extract_dims(symbol_type: ts.TypeSpec) -> list[Dimension]:
diff --git a/tox.ini b/tox.ini
index 81679887..3af6d5ab 100644
--- a/tox.ini
+++ b/tox.ini
@@ -51,3 +51,22 @@ commands =
     jupytext QuickstartGuide.md --to .py
     python QuickstartGuide.py
 commands_post =
+
+[testenv:gen_diagrams]
+install_command = echo {packages}
+skip_install = true
+allowlist_externals =
+    /bin/bash
+    make
+    gcc
+    g++
+    ldd
+    rm
+    plantuml
+    git
+    echo
+changedir = docs/functional/architecture
+commands =
+    plantuml ./*.md -tsvg -o _static
+    git add _static
+commands_post =


Test patch:
diff --git a/tests/functional_tests/ffront_tests/test_error_line_number.py b/tests/functional_tests/ffront_tests/test_error_line_number.py
index 2ff0a906..b389b6e8 100644
--- a/tests/functional_tests/ffront_tests/test_error_line_number.py
+++ b/tests/functional_tests/ffront_tests/test_error_line_number.py
@@ -1,10 +1,16 @@
+import inspect
+import traceback
+
 import pytest
 
+from functional import common
 from functional.common import Dimension, Field
+from functional.ffront import func_to_foast as f2f, source_utils as src_utils
+from functional.ffront.foast_passes import type_deduction
 from functional.ffront.func_to_foast import FieldOperatorParser, FieldOperatorSyntaxError
 
 
-# NOTE: This test is sensitive to filename and the line number of the marked statement
+# NOTE: These tests are sensitive to filename and the line number of the marked statement
 
 TDim = Dimension("TDim")  # Meaningless dimension, used for tests.
 
@@ -12,14 +18,112 @@ TDim = Dimension("TDim")  # Meaningless dimension, used for tests.
 def test_invalid_syntax_error_empty_return():
     """Field operator syntax errors point to the file, line and column."""
 
-    def wrong_syntax(inp: Field[[TDim], float]):
-        return  # <-- this line triggers error
+    line = inspect.getframeinfo(inspect.currentframe()).lineno
+
+    def wrong_syntax(inp: common.Field[[TDim], float]):
+        return  # <-- this line triggers the syntax error
 
     with pytest.raises(
-        FieldOperatorSyntaxError,
+        f2f.FieldOperatorSyntaxError,
         match=(
             r"Invalid Field Operator Syntax: "
-            r"Empty return not allowed \(test_error_line_number.py, line 16\)"
+            r"Empty return not allowed \(test_error_line_number.py, line " + str(line + 3) + r"\)"
         ),
-    ):
-        _ = FieldOperatorParser.apply_to_function(wrong_syntax)
+    ) as exc_info:
+        _ = f2f.FieldOperatorParser.apply_to_function(wrong_syntax)
+
+    assert traceback.format_exception_only(exc_info.value)[1:3] == [
+        "    return  # <-- this line triggers the syntax error\n",
+        "    ^^^^^^\n",
+    ]
+
+
+def test_wrong_caret_placement_bug():
+    """Field operator syntax errors respect python's carets (`^^^^^`) placement."""
+
+    line = inspect.getframeinfo(inspect.currentframe()).lineno
+
+    def wrong_line_syntax_error(inp: common.Field[[TDim], float]):
+        # the next line triggers the syntax error
+        inp = inp.this_attribute_surely_doesnt_exist
+
+        return inp
+
+    with pytest.raises(f2f.FieldOperatorSyntaxError) as exc_info:
+        _ = f2f.FieldOperatorParser.apply_to_function(wrong_line_syntax_error)
+
+    exc = exc_info.value
+
+    assert (exc.lineno, exc.end_lineno) == (line + 4, line + 4)
+
+    # if `offset` is set, python will display carets (`^^^^`) after printing `text`.
+    # So `text` has to be the line where the error occurs (otherwise the carets
+    # will be very misleading).
+
+    # See https://github.com/python/cpython/blob/6ad47b41a650a13b4a9214309c10239726331eb8/Lib/traceback.py#L852-L855
+    python_printed_text = exc.text.rstrip("\n").lstrip(" \n\f")
+
+    assert python_printed_text == "inp = inp.this_attribute_surely_doesnt_exist"
+
+    # test that `offset` is aligned with `exc.text`
+    return_offset = (
+        exc.text.find("inp.this_attribute_surely_doesnt_exist") + 1
+    )  # offset is 1-based for syntax errors
+    assert (exc.offset, exc.end_offset) == (return_offset, return_offset + 38)
+
+    print("".join(traceback.format_exception_only(exc)))
+
+    assert traceback.format_exception_only(exc)[1:3] == [
+        "    inp = inp.this_attribute_surely_doesnt_exist\n",
+        "          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+    ]
+
+
+def test_syntax_error_without_function():
+    """Dialect parsers report line numbers correctly when applied to `SourceDefinition`."""
+
+    source_definition = src_utils.SourceDefinition(
+        starting_line=62,
+        source="""
+            def invalid_python_syntax():
+                # This function contains a python syntax error
+
+                ret%%  # <-- this line triggers the syntax error
+        """,
+    )
+
+    with pytest.raises(SyntaxError) as exc_info:
+        _ = f2f.FieldOperatorParser.apply(source_definition, {}, {})
+
+    exc = exc_info.value
+
+    assert (exc.lineno, exc.end_lineno) == (66, 66)
+
+    assert traceback.format_exception_only(exc)[1:3] == [
+        "    ret%%  # <-- this line triggers the syntax error\n",
+        "        ^\n",
+    ]
+
+
+def test_fo_type_deduction_error():
+    """Field operator type deduction errors report location correctly"""
+
+    line = inspect.getframeinfo(inspect.currentframe()).lineno
+
+    def field_operator_with_undeclared_symbol():
+        return undeclared_symbol
+
+    with pytest.raises(type_deduction.FieldOperatorTypeDeductionError) as exc_info:
+        _ = f2f.FieldOperatorParser.apply_to_function(field_operator_with_undeclared_symbol)
+
+    exc = exc_info.value
+
+    assert (exc.lineno, exc.end_lineno) == (line + 3, line + 3)
+
+    assert traceback.format_exception_only(exc)[1:3] == [
+        "    return undeclared_symbol\n",
+        "           ^^^^^^^^^^^^^^^^^\n",
+    ]
+
+
+# TODO: test program type deduction?
diff --git a/tests/functional_tests/ffront_tests/test_execution.py b/tests/functional_tests/ffront_tests/test_execution.py
index d4d4ab8d..25ab0169 100644
--- a/tests/functional_tests/ffront_tests/test_execution.py
+++ b/tests/functional_tests/ffront_tests/test_execution.py
@@ -15,6 +15,7 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 from functools import reduce
 
+import numpy as np
 import pytest as pytest
 
 from functional.ffront.decorator import field_operator, program, scan_operator
@@ -225,6 +226,49 @@ def test_scalar_arg_with_field(fieldview_backend):
     assert np.allclose(ref, out.array())
 
 
+def test_scalar_scan():
+    size = 10
+    KDim = Dimension("K", kind=DimensionKind.VERTICAL)
+    qc = np_as_located_field(IDim, KDim)(np.zeros((size, size)))
+    scalar = 1.0
+    expected = np.full((size, size), np.arange(start=1, stop=11, step=1).astype(float64))
+
+    @scan_operator(axis=KDim, forward=True, init=(0.0))
+    def _scan_scalar(carry: float, qc_in: float, scalar: float) -> float:
+        qc = qc_in + carry + scalar
+        return qc
+
+    @program
+    def scan_scalar(qc: Field[[IDim, KDim], float], scalar: float):
+        _scan_scalar(qc, scalar, out=qc)
+
+    scan_scalar(qc, scalar, offset_provider={})
+    assert np.allclose(np.asarray(qc), expected)
+
+
+def test_tuple_scalar_scan():
+    size = 10
+    KDim = Dimension("K", kind=DimensionKind.VERTICAL)
+    qc = np_as_located_field(IDim, KDim)(np.zeros((size, size)))
+    tuple_scalar = (1.0, (1.0, 0.0))
+    expected = np.full((size, size), np.arange(start=1, stop=11, step=1).astype(float64))
+
+    @scan_operator(axis=KDim, forward=True, init=0.0)
+    def _scan_tuple_scalar(
+        state: float, qc_in: float, tuple_scalar: tuple[float, tuple[float, float]]
+    ) -> float:
+        return (qc_in + state + tuple_scalar[1][0] + tuple_scalar[1][1]) / tuple_scalar[0]
+
+    @field_operator
+    def scan_tuple_scalar(
+        qc: Field[[IDim, KDim], float], tuple_scalar: tuple[float, tuple[float, float]]
+    ) -> Field[[IDim, KDim], float]:
+        return _scan_tuple_scalar(qc, tuple_scalar)
+
+    scan_tuple_scalar(qc, tuple_scalar, out=qc, offset_provider={})
+    assert np.allclose(np.asarray(qc), expected)
+
+
 def test_astype_int(fieldview_backend):
     size = 10
     b_float_64 = np_as_located_field(IDim)(np.ones((size), dtype=np.float64))
@@ -744,8 +788,6 @@ def test_where_k_offset(fieldview_backend):
 
 
 def test_undefined_symbols():
-    from functional.ffront.foast_passes.type_deduction import FieldOperatorTypeDeductionError
-
     with pytest.raises(FieldOperatorTypeDeductionError, match="Undeclared symbol"):
 
         @field_operator
@@ -800,7 +842,7 @@ def test_tuple_unpacking(fieldview_backend):
     out4 = np_as_located_field(IDim)(np.ones((size)))
 
     @field_operator
-    def _unpack(
+    def unpack(
         inp: Field[[IDim], float64],
     ) -> tuple[
         Field[[IDim], float64],
@@ -811,17 +853,7 @@ def test_tuple_unpacking(fieldview_backend):
         a, b, c, d = (inp + 2.0, inp + 3.0, inp + 5.0, inp + 7.0)
         return a, b, c, d
 
-    @program(backend=fieldview_backend)
-    def unpack(
-        inp: Field[[IDim], float64],
-        out1: Field[[IDim], float64],
-        out2: Field[[IDim], float64],
-        out3: Field[[IDim], float64],
-        out4: Field[[IDim], float64],
-    ):
-        _unpack(inp, out=(out1, out2, out3, out4))
-
-    unpack(inp, out1, out2, out3, out4, offset_provider={})
+    unpack(inp, out=(out1, out2, out3, out4), offset_provider={})
 
     arr = inp.array()
 
diff --git a/tests/functional_tests/ffront_tests/test_foast_lowering.py b/tests/functional_tests/ffront_tests/test_foast_lowering.py
index 57dfb0ff..81e4ede8 100644
--- a/tests/functional_tests/ffront_tests/test_foast_lowering.py
+++ b/tests/functional_tests/ffront_tests/test_foast_lowering.py
@@ -60,7 +60,7 @@ def test_copy():
     lowered = FieldOperatorLowering.apply(parsed)
 
     assert lowered.id == "copy_field"
-    assert lowered.expr == im.deref_("inp")
+    assert lowered.expr == im.ref("inp")
 
 
 def test_scalar_arg():
@@ -71,9 +71,7 @@ def test_scalar_arg():
     parsed = FieldOperatorParser.apply_to_function(scalar_arg)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("bar")(im.multiplies_("alpha", im.deref_("bar"))))("bar")
-    )
+    reference = im.lift_(im.lambda__("bar")(im.multiplies_("alpha", im.deref_("bar"))))("bar")
 
     assert lowered.expr == reference
 
@@ -85,11 +83,9 @@ def test_multicopy():
     parsed = FieldOperatorParser.apply_to_function(multicopy)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("inp1", "inp2")(im.make_tuple_(im.deref_("inp1"), im.deref_("inp2"))))(
-            "inp1", "inp2"
-        )
-    )
+    reference = im.lift_(
+        im.lambda__("inp1", "inp2")(im.make_tuple_(im.deref_("inp1"), im.deref_("inp2")))
+    )("inp1", "inp2")
 
     assert lowered.expr == reference
 
@@ -102,11 +98,9 @@ def test_arithmetic():
     parsed = FieldOperatorParser.apply_to_function(arithmetic)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("inp1", "inp2")(im.plus_(im.deref_("inp1"), im.deref_("inp2"))))(
-            "inp1", "inp2"
-        )
-    )
+    reference = im.lift_(
+        im.lambda__("inp1", "inp2")(im.plus_(im.deref_("inp1"), im.deref_("inp2")))
+    )("inp1", "inp2")
 
     assert lowered.expr == reference
 
@@ -121,7 +115,7 @@ def test_shift():
     parsed = FieldOperatorParser.apply_to_function(shift_by_one)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(im.shift_("Ioff", 1)("inp"))
+    reference = im.shift_("Ioff", 1)("inp")
 
     assert lowered.expr == reference
 
@@ -136,7 +130,7 @@ def test_negative_shift():
     parsed = FieldOperatorParser.apply_to_function(shift_by_one)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(im.shift_("Ioff", -1)("inp"))
+    reference = im.shift_("Ioff", -1)("inp")
 
     assert lowered.expr == reference
 
@@ -152,7 +146,7 @@ def test_temp_assignment():
     lowered = FieldOperatorLowering.apply(parsed)
 
     reference = im.let("tmp__0", "inp")(
-        im.let("inp__0", "tmp__0")(im.let("tmp2__0", "inp__0")(im.deref_("tmp2__0")))
+        im.let("inp__0", "tmp__0")(im.let("tmp2__0", "inp__0")("tmp2__0"))
     )
 
     assert lowered.expr == reference
@@ -174,7 +168,7 @@ def test_unary_ops():
         im.let(
             "tmp__1",
             im.lift_(im.lambda__("tmp__0")(im.minus_(0, im.deref_("tmp__0"))))("tmp__0"),
-        )(im.deref_("tmp__1"))
+        )("tmp__1")
     )
 
     assert lowered.expr == reference
@@ -203,7 +197,7 @@ def test_unpacking():
     )("__tuple_tmp_0")
 
     reference = im.let("__tuple_tmp_0", tuple_expr)(
-        im.let("tmp1__0", tuple_access_0)(im.let("tmp2__0", tuple_access_1)(im.deref_("tmp1__0")))
+        im.let("tmp1__0", tuple_access_0)(im.let("tmp2__0", tuple_access_1)("tmp1__0"))
     )
     assert lowered.expr == reference
 
@@ -218,7 +212,7 @@ def test_annotated_assignment():
     parsed = FieldOperatorParser.apply_to_function(copy_field)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.let("tmp__0", "inp")(im.deref_("tmp__0"))
+    reference = im.let("tmp__0", "inp")("tmp__0")
 
     assert lowered.expr == reference
 
@@ -240,7 +234,7 @@ def test_call():
     parsed = FieldOperatorParser.apply_to_function(call)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(im.lift_(im.lambda__("inp")(im.call_("identity")("inp")))("inp"))
+    reference = im.lift_(im.lambda__("inp")(im.call_("identity")("inp")))("inp")
 
     assert lowered.expr == reference
 
@@ -258,7 +252,7 @@ def test_temp_tuple():
     tuple_expr = im.lift_(im.lambda__("a", "b")(im.make_tuple_(im.deref_("a"), im.deref_("b"))))(
         "a", "b"
     )
-    reference = im.let("tmp__0", tuple_expr)(im.deref_("tmp__0"))
+    reference = im.let("tmp__0", tuple_expr)("tmp__0")
 
     assert lowered.expr == reference
 
@@ -270,7 +264,7 @@ def test_unary_not():
     parsed = FieldOperatorParser.apply_to_function(unary_not)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(im.lift_(im.lambda__("cond")(im.not__(im.deref_("cond"))))("cond"))
+    reference = im.lift_(im.lambda__("cond")(im.not__(im.deref_("cond"))))("cond")
 
     assert lowered.expr == reference
 
@@ -282,9 +276,7 @@ def test_binary_plus():
     parsed = FieldOperatorParser.apply_to_function(plus)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.plus_(im.deref_("a"), im.deref_("b"))))("a", "b")
-    )
+    reference = im.lift_(im.lambda__("a", "b")(im.plus_(im.deref_("a"), im.deref_("b"))))("a", "b")
 
     assert lowered.expr == reference
 
@@ -296,8 +288,8 @@ def test_add_scalar_literal_to_field():
     parsed = FieldOperatorParser.apply_to_function(scalar_plus_field)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a")(im.plus_(im.literal_("2.0", "float64"), im.deref_("a"))))("a")
+    reference = im.lift_(im.lambda__("a")(im.plus_(im.literal_("2.0", "float64"), im.deref_("a"))))(
+        "a"
     )
 
     assert lowered.expr == reference
@@ -317,7 +309,7 @@ def test_add_scalar_literals():
             im.literal_("1", "int32"),
             im.literal_("1", "int32"),
         ),
-    )(im.deref_(im.lift_(im.lambda__("a")(im.plus_(im.deref_("a"), "tmp__0")))("a")))
+    )(im.lift_(im.lambda__("a")(im.plus_(im.deref_("a"), "tmp__0")))("a"))
 
     assert lowered.expr == reference
 
@@ -329,8 +321,8 @@ def test_binary_mult():
     parsed = FieldOperatorParser.apply_to_function(mult)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.multiplies_(im.deref_("a"), im.deref_("b"))))("a", "b")
+    reference = im.lift_(im.lambda__("a", "b")(im.multiplies_(im.deref_("a"), im.deref_("b"))))(
+        "a", "b"
     )
 
     assert lowered.expr == reference
@@ -343,9 +335,7 @@ def test_binary_minus():
     parsed = FieldOperatorParser.apply_to_function(minus)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.minus_(im.deref_("a"), im.deref_("b"))))("a", "b")
-    )
+    reference = im.lift_(im.lambda__("a", "b")(im.minus_(im.deref_("a"), im.deref_("b"))))("a", "b")
 
     assert lowered.expr == reference
 
@@ -357,8 +347,8 @@ def test_binary_div():
     parsed = FieldOperatorParser.apply_to_function(division)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.divides_(im.deref_("a"), im.deref_("b"))))("a", "b")
+    reference = im.lift_(im.lambda__("a", "b")(im.divides_(im.deref_("a"), im.deref_("b"))))(
+        "a", "b"
     )
 
     assert lowered.expr == reference
@@ -371,9 +361,7 @@ def test_binary_and():
     parsed = FieldOperatorParser.apply_to_function(bit_and)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.and__(im.deref_("a"), im.deref_("b"))))("a", "b")
-    )
+    reference = im.lift_(im.lambda__("a", "b")(im.and__(im.deref_("a"), im.deref_("b"))))("a", "b")
 
     assert lowered.expr == reference
 
@@ -385,8 +373,8 @@ def test_scalar_and():
     parsed = FieldOperatorParser.apply_to_function(scalar_and)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a")(im.and__(im.deref_("a"), im.literal_("False", "bool"))))("a")
+    reference = im.lift_(im.lambda__("a")(im.and__(im.deref_("a"), im.literal_("False", "bool"))))(
+        "a"
     )
 
     assert lowered.expr == reference
@@ -399,9 +387,7 @@ def test_binary_or():
     parsed = FieldOperatorParser.apply_to_function(bit_or)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.or__(im.deref_("a"), im.deref_("b"))))("a", "b")
-    )
+    reference = im.lift_(im.lambda__("a", "b")(im.or__(im.deref_("a"), im.deref_("b"))))("a", "b")
 
     assert lowered.expr == reference
 
@@ -425,8 +411,8 @@ def test_compare_gt():
     parsed = FieldOperatorParser.apply_to_function(comp_gt)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.greater_(im.deref_("a"), im.deref_("b"))))("a", "b")
+    reference = im.lift_(im.lambda__("a", "b")(im.greater_(im.deref_("a"), im.deref_("b"))))(
+        "a", "b"
     )
 
     assert lowered.expr == reference
@@ -439,9 +425,7 @@ def test_compare_lt():
     parsed = FieldOperatorParser.apply_to_function(comp_lt)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.less_(im.deref_("a"), im.deref_("b"))))("a", "b")
-    )
+    reference = im.lift_(im.lambda__("a", "b")(im.less_(im.deref_("a"), im.deref_("b"))))("a", "b")
 
     assert lowered.expr == reference
 
@@ -453,9 +437,7 @@ def test_compare_eq():
     parsed = FieldOperatorParser.apply_to_function(comp_eq)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(im.lambda__("a", "b")(im.eq_(im.deref_("a"), im.deref_("b"))))("a", "b")
-    )
+    reference = im.lift_(im.lambda__("a", "b")(im.eq_(im.deref_("a"), im.deref_("b"))))("a", "b")
 
     assert lowered.expr == reference
 
@@ -469,24 +451,22 @@ def test_compare_chain():
     parsed = FieldOperatorParser.apply_to_function(compare_chain)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(
-            im.lambda__("a", "b", "c")(
-                im.and__(
-                    im.deref_(
-                        im.lift_(
-                            im.lambda__("a", "b")(im.greater_(im.deref_("a"), im.deref_("b")))
-                        )("a", "b")
-                    ),
-                    im.deref_(
-                        im.lift_(
-                            im.lambda__("b", "c")(im.greater_(im.deref_("b"), im.deref_("c")))
-                        )("b", "c")
-                    ),
-                )
+    reference = im.lift_(
+        im.lambda__("a", "b", "c")(
+            im.and__(
+                im.deref_(
+                    im.lift_(im.lambda__("a", "b")(im.greater_(im.deref_("a"), im.deref_("b"))))(
+                        "a", "b"
+                    )
+                ),
+                im.deref_(
+                    im.lift_(im.lambda__("b", "c")(im.greater_(im.deref_("b"), im.deref_("c"))))(
+                        "b", "c"
+                    )
+                ),
             )
-        )("a", "b", "c")
-    )
+        )
+    )("a", "b", "c")
 
     assert lowered.expr == reference
 
@@ -498,14 +478,12 @@ def test_reduction_lowering_simple():
     parsed = FieldOperatorParser.apply_to_function(reduction)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.deref_(
-        im.lift_(
-            im.call_("reduce")(
-                im.lambda__("acc", "edge_f__0")(im.plus_("acc", "edge_f__0")),
-                0,
-            )
-        )(im.shift_("V2E")("edge_f"))
-    )
+    reference = im.lift_(
+        im.call_("reduce")(
+            im.lambda__("acc", "edge_f__0")(im.plus_("acc", "edge_f__0")),
+            0,
+        )
+    )(im.shift_("V2E")("edge_f"))
 
     assert lowered.expr == reference
 
@@ -519,21 +497,19 @@ def test_reduction_lowering_expr():
     lowered = FieldOperatorLowering.apply(parsed)
 
     reference = im.let("e1_nbh__0", im.shift_("V2E")("e1"))(
-        im.deref_(
-            im.lift_(
-                im.call_("reduce")(
-                    im.lambda__("acc", "e1_nbh__0__0", "e2__1")(
-                        im.plus_(
-                            "acc",
-                            im.multiplies_(
-                                im.literal_("1.1", "float64"), im.plus_("e1_nbh__0__0", "e2__1")
-                            ),
-                        )
-                    ),
-                    0,
-                )
-            )("e1_nbh__0", "e2")
-        )
+        im.lift_(
+            im.call_("reduce")(
+                im.lambda__("acc", "e1_nbh__0__0", "e2__1")(
+                    im.plus_(
+                        "acc",
+                        im.multiplies_(
+                            im.literal_("1.1", "float64"), im.plus_("e1_nbh__0__0", "e2__1")
+                        ),
+                    )
+                ),
+                0,
+            )
+        )("e1_nbh__0", "e2")
     )
 
     assert lowered.expr == reference
diff --git a/tests/functional_tests/ffront_tests/test_foast_parsing.py b/tests/functional_tests/ffront_tests/test_foast_parsing.py
index 00eb16af..02a64eb6 100644
--- a/tests/functional_tests/ffront_tests/test_foast_parsing.py
+++ b/tests/functional_tests/ffront_tests/test_foast_parsing.py
@@ -117,7 +117,7 @@ def test_return_type():
 
     parsed = FieldOperatorParser.apply_to_function(rettype)
 
-    assert parsed.body[-1].value.type == ts.FieldType(
+    assert parsed.body.stmts[-1].value.type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
@@ -131,7 +131,7 @@ def test_invalid_syntax_no_return():
 
     with pytest.raises(
         FieldOperatorSyntaxError,
-        match="Field operator must return a field expression on the last line!",
+        match="Function must return a value, but no return statement was found\.",
     ):
         _ = FieldOperatorParser.apply_to_function(no_return)
 
@@ -157,7 +157,7 @@ def test_temp_assignment():
 
     parsed = FieldOperatorParser.apply_to_function(copy_field)
 
-    assert parsed.annex.symtable["tmp__0"].type == ts.FieldType(
+    assert parsed.body.annex.symtable["tmp__0"].type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
@@ -180,7 +180,7 @@ def test_binary_pow():
 
     parsed = FieldOperatorParser.apply_to_function(power)
 
-    assert parsed.body[-1].value.type == ts.FieldType(
+    assert parsed.body.stmts[-1].value.type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
@@ -192,7 +192,7 @@ def test_binary_mod():
 
     parsed = FieldOperatorParser.apply_to_function(modulo)
 
-    assert parsed.body[-1].value.type == ts.FieldType(
+    assert parsed.body.stmts[-1].value.type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.INT64, shape=None),
     )
@@ -226,7 +226,7 @@ def test_bool_xor():
 
     parsed = FieldOperatorParser.apply_to_function(bool_xor)
 
-    assert parsed.body[-1].value.type == ts.FieldType(
+    assert parsed.body.stmts[-1].value.type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.BOOL, shape=None),
     )
@@ -238,7 +238,7 @@ def test_unary_tilde():
 
     parsed = FieldOperatorParser.apply_to_function(unary_tilde)
 
-    assert parsed.body[-1].value.type == ts.FieldType(
+    assert parsed.body.stmts[-1].value.type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.BOOL, shape=None),
     )
@@ -333,7 +333,7 @@ def test_astype():
 
     parsed = FieldOperatorParser.apply_to_function(astype_fieldop)
 
-    assert parsed.body[-1].value.type == ts.FieldType(
+    assert parsed.body.stmts[-1].value.type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
@@ -354,22 +354,25 @@ def test_closure_symbols():
         return a, b
 
     parsed = FieldOperatorParser.apply_to_function(operator_with_refs)
-    assert "nonlocals_unused" not in parsed.annex.symtable
-    assert "nonlocals" not in parsed.annex.symtable
+    assert "nonlocals_unreferenced" not in {**parsed.annex.symtable, **parsed.body.annex.symtable}
+    assert "nonlocals" not in {**parsed.annex.symtable, **parsed.body.annex.symtable}
 
     pattern_node = P(
         foast.FunctionDefinition,
-        body=[
-            P(
-                foast.Assign,
-                value=P(foast.BinOp, right=P(foast.Constant, value=nonlocals.float_value)),
-            ),
-            P(
-                foast.Assign,
-                value=P(foast.BinOp, right=P(foast.Constant, value=nonlocals.np_value)),
-            ),
-            P(foast.Return),
-        ],
+        body=P(
+            foast.BlockStmt,
+            stmts=[
+                P(
+                    foast.Assign,
+                    value=P(foast.BinOp, right=P(foast.Constant, value=nonlocals.float_value)),
+                ),
+                P(
+                    foast.Assign,
+                    value=P(foast.BinOp, right=P(foast.Constant, value=nonlocals.np_value)),
+                ),
+                P(foast.Return),
+            ],
+        ),
     )
     assert pattern_node.match(parsed, raise_exception=True)
 
diff --git a/tests/functional_tests/ffront_tests/test_foast_pretty_printer.py b/tests/functional_tests/ffront_tests/test_foast_pretty_printer.py
index 91671f4e..39659f7d 100644
--- a/tests/functional_tests/ffront_tests/test_foast_pretty_printer.py
+++ b/tests/functional_tests/ffront_tests/test_foast_pretty_printer.py
@@ -62,12 +62,14 @@ def test_scanop():
 
     @scan_operator(axis=KDim, forward=False, init=1.0)
     def scan(inp: int64) -> int64:
+        foo = inp
         return inp
 
     expected = textwrap.dedent(
         """
         @scan_operator(axis=Dimension(value="KDim", kind=DimensionKind.VERTICAL), forward=False, init=1.0)
         def scan(inp: int64) -> int64:
+          foo__0 = inp
           return inp
         """
     ).strip()
diff --git a/tests/functional_tests/ffront_tests/test_math_builtin_execution.py b/tests/functional_tests/ffront_tests/test_math_builtin_execution.py
index 2360f392..ec158678 100644
--- a/tests/functional_tests/ffront_tests/test_math_builtin_execution.py
+++ b/tests/functional_tests/ffront_tests/test_math_builtin_execution.py
@@ -74,17 +74,20 @@ def make_builtin_field_operator(builtin_name: str):
         id=builtin_name + "_field_operator",
         definition=foast.FunctionDefinition(
             id=builtin_name + "_field_operator",
-            body=[
-                foast.Return(
-                    value=foast.Call(
-                        func=foast.Name(id=builtin_name, location=loc),
-                        args=args,
-                        kwargs={},
+            body=foast.BlockStmt(
+                stmts=[
+                    foast.Return(
+                        value=foast.Call(
+                            func=foast.Name(id=builtin_name, location=loc),
+                            args=args,
+                            kwargs={},
+                            location=loc,
+                        ),
                         location=loc,
-                    ),
-                    location=loc,
-                )
-            ],
+                    )
+                ],
+                location=loc,
+            ),
             closure_vars=closure_var_symbols,
             params=params,
             location=loc,
diff --git a/tests/functional_tests/ffront_tests/test_type_deduction.py b/tests/functional_tests/ffront_tests/test_type_deduction.py
index c117e83d..912c48bf 100644
--- a/tests/functional_tests/ffront_tests/test_type_deduction.py
+++ b/tests/functional_tests/ffront_tests/test_type_deduction.py
@@ -300,11 +300,11 @@ def test_unpack_assign():
 
     parsed = FieldOperatorParser.apply_to_function(unpack_explicit_tuple)
 
-    assert parsed.annex.symtable["tmp_a__0"].type == ts.FieldType(
+    assert parsed.body.annex.symtable["tmp_a__0"].type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
-    assert parsed.annex.symtable["tmp_b__0"].type == ts.FieldType(
+    assert parsed.body.annex.symtable["tmp_b__0"].type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
@@ -362,7 +362,7 @@ def test_assign_tuple():
 
     parsed = FieldOperatorParser.apply_to_function(temp_tuple)
 
-    assert parsed.annex.symtable["tmp__0"].type == ts.TupleType(
+    assert parsed.body.annex.symtable["tmp__0"].type == ts.TupleType(
         types=[
             ts.FieldType(
                 dims=[TDim],
@@ -456,7 +456,7 @@ def test_remap(remap_setup):
 
     parsed = FieldOperatorParser.apply_to_function(remap_fo)
 
-    assert parsed.body[0].value.type == ts.FieldType(
+    assert parsed.body.stmts[0].value.type == ts.FieldType(
         dims=[Y], dtype=ts.ScalarType(kind=ts.ScalarKind.INT64)
     )
 
@@ -469,7 +469,7 @@ def test_remap_nbfield(remap_setup):
 
     parsed = FieldOperatorParser.apply_to_function(remap_fo)
 
-    assert parsed.body[0].value.type == ts.FieldType(
+    assert parsed.body.stmts[0].value.type == ts.FieldType(
         dims=[Y, Y2XDim], dtype=ts.ScalarType(kind=ts.ScalarKind.INT64)
     )
 
@@ -482,7 +482,7 @@ def test_remap_reduce(remap_setup):
 
     parsed = FieldOperatorParser.apply_to_function(remap_fo)
 
-    assert parsed.body[0].value.type == ts.FieldType(
+    assert parsed.body.stmts[0].value.type == ts.FieldType(
         dims=[Y], dtype=ts.ScalarType(kind=ts.ScalarKind.INT64)
     )
 
@@ -495,7 +495,7 @@ def test_remap_reduce_sparse(remap_setup):
 
     parsed = FieldOperatorParser.apply_to_function(remap_fo)
 
-    assert parsed.body[0].value.type == ts.FieldType(
+    assert parsed.body.stmts[0].value.type == ts.FieldType(
         dims=[Y], dtype=ts.ScalarType(kind=ts.ScalarKind.INT64)
     )
 
@@ -521,7 +521,7 @@ def test_broadcast_multi_dim():
 
     parsed = FieldOperatorParser.apply_to_function(simple_broadcast)
 
-    assert parsed.body[0].value.type == ts.FieldType(
+    assert parsed.body.stmts[0].value.type == ts.FieldType(
         dims=[ADim, BDim, CDim], dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64)
     )
 
@@ -565,7 +565,7 @@ def test_where_dim():
 
     parsed = FieldOperatorParser.apply_to_function(simple_where)
 
-    assert parsed.body[0].value.type == ts.FieldType(
+    assert parsed.body.stmts[0].value.type == ts.FieldType(
         dims=[ADim, BDim], dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64)
     )
 
@@ -578,7 +578,7 @@ def test_where_broadcast_dim():
 
     parsed = FieldOperatorParser.apply_to_function(simple_where)
 
-    assert parsed.body[0].value.type == ts.FieldType(
+    assert parsed.body.stmts[0].value.type == ts.FieldType(
         dims=[ADim], dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64)
     )
 
@@ -591,7 +591,7 @@ def test_where_tuple_dim():
 
     parsed = FieldOperatorParser.apply_to_function(tuple_where)
 
-    assert parsed.body[0].value.type == ts.TupleType(
+    assert parsed.body.stmts[0].value.type == ts.TupleType(
         types=[
             ts.TupleType(
                 types=[
@@ -633,7 +633,7 @@ def test_where_mixed_dims():
 
     parsed = FieldOperatorParser.apply_to_function(tuple_where_mix_dims)
 
-    assert parsed.body[0].value.type == ts.TupleType(
+    assert parsed.body.stmts[0].value.type == ts.TupleType(
         types=[
             ts.TupleType(
                 types=[
@@ -661,7 +661,7 @@ def test_astype_dtype():
 
     parsed = FieldOperatorParser.apply_to_function(simple_astype)
 
-    assert parsed.body[0].value.type == ts.FieldType(
+    assert parsed.body.stmts[0].value.type == ts.FieldType(
         dims=[ADim], dtype=ts.ScalarType(kind=ts.ScalarKind.BOOL)
     )
 
diff --git a/tests/functional_tests/iterator_tests/test_propagate_deref.py b/tests/functional_tests/iterator_tests/test_propagate_deref.py
new file mode 100644
index 00000000..4d4ca9ee
--- /dev/null
+++ b/tests/functional_tests/iterator_tests/test_propagate_deref.py
@@ -0,0 +1,14 @@
+from functional.ffront import itir_makers as im
+from functional.iterator.transforms.propagate_deref import PropagateDeref
+
+
+def test_deref_propagation():
+    testee = im.deref_(
+        im.call_(im.lambda__("inner_it")(im.lift_("stencil")("inner_it")))("outer_it")
+    )
+    expected = im.call_(im.lambda__("inner_it")(im.deref_(im.lift_("stencil")("inner_it"))))(
+        "outer_it"
+    )
+
+    actual = PropagateDeref.apply(testee)
+    assert actual == expected