Skip to content

Commit 9a362de

Browse files
committed
Cache C for-in iterable expressions
1 parent 27261bd commit 9a362de

6 files changed

Lines changed: 78 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5252
- **Fallthrough handling**
5353
- `fall` now fails closed with a semantic diagnostic, and both backends raise codegen errors if a `FALL` node reaches them.
5454

55+
- **C backend iteration**
56+
- `for-in` and indexed `for-in` now cache array/slice iterable expressions before loop lowering so side-effectful iterables are evaluated once.
57+
5558
- **Semantic and preprocessing correctness**
5659
- `defer` now traverses its parsed `statement` payload in both type checking and semantic validation.
5760
- `ret` semantic validation now traverses the parser's `value` payload.

MISSING_FEATURES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
17. Local file-based imports now fail closed during semantic analysis instead of swallowing module loading failures.
3232
18. Zig unsupported expression fallbacks now fail as compiler-side codegen errors instead of generated `@compileError` expressions.
3333
19. `fall` now fails closed with a semantic error instead of being ignored or reaching backend output.
34+
20. C backend `for-in` lowering now caches array/slice iterable expressions so side-effectful iterables are evaluated once.
3435

3536
---
3637

RELEASE_READINESS_REVIEW.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ not factually provable from local tests alone.
4848
build.
4949
- GitHub release workflow now creates a draft release for `v*` tags with Python
5050
package artifacts, docs site archive, and release example artifacts.
51+
- C backend `for-in` lowering now caches iterable expressions before loop
52+
length and element access.
5153
- GitHub Pages deploy now uses `npm ci` with `site/package-lock.json`.
5254
- README, SPEC, release docs, status docs, and agent docs describe the same
5355
release commands.

TODO.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ These are bugs and schema mismatches in already-implemented features.
2525
Files: `src/backends/zig.py`
2626
Notes: unsupported expression nodes now raise `CodegenError` during A7 compilation.
2727

28-
- [ ] Stop C slice/iteration lowering from re-evaluating side-effectful expressions.
28+
- [x] Stop C slice/iteration lowering from re-evaluating side-effectful expressions.
2929
Files: `src/backends/c.py`
30-
Notes: `for-in` and sub-slice lowering reuse the iterable expression multiple times, duplicating side effects.
30+
Notes: `for-in` and indexed `for-in` now cache array/slice iterable expressions in a generated local before loop length and element access.
3131

3232
- [x] Reject non-iterables in `for-in` and indexed `for-in` during type checking.
3333
Files: `src/passes/type_checker.py`

src/backends/c.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -776,12 +776,19 @@ def _visit_for_in(self, node: ASTNode) -> None:
776776

777777
iterable_expr = self._emit_expr(node.iterable)
778778
iterable_type = self._type_map.get(id(node.iterable)) if node.iterable else None
779+
cache_name = self._unique_name("__a7_iter")
780+
cache_type = self._iterable_cache_type(iterable_type)
779781
elem_type = self._iterable_element_type(iterable_type)
780-
length_expr = self._iterable_length_expr(node.iterable, iterable_expr, iterable_type)
782+
length_expr = self._iterable_length_expr(node.iterable, cache_name, iterable_type)
781783

782784
idx_name = self._unique_name("__a7_i")
783785
iter_name = self._sanitize_name(node.iterator or "item")
784786

787+
self._write_indent()
788+
self.output.write("{\n")
789+
self.indent()
790+
self._write_indent()
791+
self.output.write(f"{cache_type} {cache_name} = {iterable_expr};\n")
785792
self._write_indent()
786793
self.output.write(
787794
f"for (size_t {idx_name} = 0; {idx_name} < {length_expr}; ++{idx_name}) "
@@ -791,7 +798,7 @@ def _visit_for_in(self, node: ASTNode) -> None:
791798
self._defer_scopes.append([])
792799
self._write_indent()
793800
self.output.write(
794-
f"{elem_type} {iter_name} = {self._emit_iterable_element_expr(node.iterable, iterable_expr, iterable_type, idx_name)};\n"
801+
f"{elem_type} {iter_name} = {self._emit_iterable_element_expr(node.iterable, cache_name, iterable_type, idx_name)};\n"
795802
)
796803

797804
marker = len(self._defer_scopes) - 1
@@ -808,6 +815,9 @@ def _visit_for_in(self, node: ASTNode) -> None:
808815
self.dedent()
809816
self._write_indent()
810817
self.output.write("}\n")
818+
self.dedent()
819+
self._write_indent()
820+
self.output.write("}\n")
811821

812822
def _visit_for_in_indexed(self, node: ASTNode) -> None:
813823
if node.label:
@@ -816,13 +826,20 @@ def _visit_for_in_indexed(self, node: ASTNode) -> None:
816826

817827
iterable_expr = self._emit_expr(node.iterable)
818828
iterable_type = self._type_map.get(id(node.iterable)) if node.iterable else None
829+
cache_name = self._unique_name("__a7_iter")
830+
cache_type = self._iterable_cache_type(iterable_type)
819831
elem_type = self._iterable_element_type(iterable_type)
820-
length_expr = self._iterable_length_expr(node.iterable, iterable_expr, iterable_type)
832+
length_expr = self._iterable_length_expr(node.iterable, cache_name, iterable_type)
821833

822834
idx_name = self._unique_name("__a7_i")
823835
index_var = self._sanitize_name(node.index_var or "index")
824836
iter_name = self._sanitize_name(node.iterator or "item")
825837

838+
self._write_indent()
839+
self.output.write("{\n")
840+
self.indent()
841+
self._write_indent()
842+
self.output.write(f"{cache_type} {cache_name} = {iterable_expr};\n")
826843
self._write_indent()
827844
self.output.write(
828845
f"for (size_t {idx_name} = 0; {idx_name} < {length_expr}; ++{idx_name}) "
@@ -834,7 +851,7 @@ def _visit_for_in_indexed(self, node: ASTNode) -> None:
834851
self.output.write(f"size_t {index_var} = {idx_name};\n")
835852
self._write_indent()
836853
self.output.write(
837-
f"{elem_type} {iter_name} = {self._emit_iterable_element_expr(node.iterable, iterable_expr, iterable_type, idx_name)};\n"
854+
f"{elem_type} {iter_name} = {self._emit_iterable_element_expr(node.iterable, cache_name, iterable_type, idx_name)};\n"
838855
)
839856

840857
marker = len(self._defer_scopes) - 1
@@ -851,6 +868,9 @@ def _visit_for_in_indexed(self, node: ASTNode) -> None:
851868
self.dedent()
852869
self._write_indent()
853870
self.output.write("}\n")
871+
self.dedent()
872+
self._write_indent()
873+
self.output.write("}\n")
854874

855875
def _visit_match(self, node: ASTNode) -> None:
856876
expr = self._emit_expr(node.expression) if node.expression else "0"
@@ -1720,14 +1740,18 @@ def _visit_labeled_for_in(self, node: ASTNode, *, indexed: bool) -> None:
17201740
idx_name = self._unique_name("__a7_i")
17211741
iterable_expr = self._emit_expr(node.iterable)
17221742
iterable_type = self._type_map.get(id(node.iterable)) if node.iterable else None
1743+
cache_name = self._unique_name("__a7_iter")
1744+
cache_type = self._iterable_cache_type(iterable_type)
17231745
elem_type = self._iterable_element_type(iterable_type)
1724-
length_expr = self._iterable_length_expr(node.iterable, iterable_expr, iterable_type)
1746+
length_expr = self._iterable_length_expr(node.iterable, cache_name, iterable_type)
17251747
iter_name = self._sanitize_name(node.iterator or "item")
17261748

17271749
self._write_indent()
17281750
self.output.write("{\n")
17291751
self.indent()
17301752
self._write_indent()
1753+
self.output.write(f"{cache_type} {cache_name} = {iterable_expr};\n")
1754+
self._write_indent()
17311755
self.output.write(f"size_t {idx_name} = 0;\n")
17321756
self._write_indent()
17331757
self.output.write(f"{cond_label}:\n")
@@ -1743,7 +1767,7 @@ def _visit_labeled_for_in(self, node: ASTNode, *, indexed: bool) -> None:
17431767
self.output.write(f"size_t {index_var} = {idx_name};\n")
17441768
self._write_indent()
17451769
self.output.write(
1746-
f"{elem_type} {iter_name} = {self._emit_iterable_element_expr(node.iterable, iterable_expr, iterable_type, idx_name)};\n"
1770+
f"{elem_type} {iter_name} = {self._emit_iterable_element_expr(node.iterable, cache_name, iterable_type, idx_name)};\n"
17471771
)
17481772
marker = len(self._defer_scopes) - 1
17491773
self._push_loop_frame(
@@ -1836,6 +1860,17 @@ def _emit_iterable_element_expr(
18361860
return f"({iterable_expr}).data[{idx_name}]"
18371861
return f"{iterable_expr}[{idx_name}]"
18381862

1863+
def _iterable_cache_type(self, iterable_type) -> str:
1864+
if isinstance(iterable_type, SliceType):
1865+
return self._semantic_type_to_c(iterable_type) or "void*"
1866+
if isinstance(iterable_type, ArrayType):
1867+
elem = self._semantic_type_to_c(iterable_type.element_type) or "int32_t"
1868+
return f"{elem}*"
1869+
raise CodegenError(
1870+
"C backend: for-in iteration currently requires an array or slice value",
1871+
None,
1872+
)
1873+
18391874
def _iterable_element_type(self, iterable_type) -> str:
18401875
if isinstance(iterable_type, ArrayType):
18411876
return self._semantic_type_to_c(iterable_type.element_type) or "int32_t"

test/test_codegen_c.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,35 @@ def test_generated_c_supports_slice_expr_index_and_for_in(tmp_path: Path) -> Non
161161
assert result.stdout.strip() == "11 12"
162162

163163

164+
@pytest.mark.skipif(not has_zig(), reason="zig not installed")
165+
def test_generated_c_for_in_caches_side_effectful_slice_iterable(tmp_path: Path) -> None:
166+
result = build_and_run_c(
167+
"""
168+
io :: import "std/io"
169+
170+
calls := 0
171+
172+
make_tail :: fn(arr: [4]i32) []i32 {
173+
calls += 1
174+
ret arr[1..4]
175+
}
176+
177+
main :: fn() {
178+
arr: [4]i32 = [1, 2, 3, 4]
179+
total := 0
180+
for x in make_tail(arr) {
181+
total += x
182+
}
183+
io.println("{} {}", total, calls)
184+
}
185+
""",
186+
tmp_path,
187+
)
188+
189+
assert result.returncode == 0, result.stdout + result.stderr
190+
assert result.stdout.strip() == "9 1"
191+
192+
164193
@pytest.mark.skipif(not has_zig(), reason="zig not installed")
165194
def test_generated_c_honors_labeled_break_and_continue(tmp_path: Path) -> None:
166195
result = build_and_run_c(

0 commit comments

Comments
 (0)