Loops GitHub issue

vibecode
{"vibecode": {
    "doc": "loops",
    "role": "all_loop_constructs_in_caspian_in_one_place",
    "loop_forms": ["while_block", "each_iteration_method", "numeric_helpers_times_upto_downto"],
    "loop_object_via": "as_loop",
    "loop_object_methods": ["next", "return", "break", "count", "active", "index"],
    "loop_exit_methods_aliased": ["return", "break"],
    "loop_exit_bwc": "break_optional_level_count",
    "structural_blocks": ["before", "between", "after", "noloop"],
    "notes": ["structural_blocks_have_no_access_to_iteration_variable",
        "as_keyword_is_a_general_block_mechanism_see_caspian_md_the_as_keyword_section",
        "break_bwc_added_post_soft_lock_2026-05-17_as_deliberate_v1_addition"]
}}

Caspian has three ways to loop:

Any of them can be named with as $loop to bind a loop object that exposes iteration state and control methods. The general as keyword mechanism (which applies to any block, not just loops) is covered in caspian.md § The as Keyword; this doc focuses on the loop-specific use.


Main structures GitHub issue

vibecode
{"vibecode": {
    "section": "main_structures",
    "forms": ["while", "each", "numeric_iteration_helpers"]
}}

Caspian offers three main looping forms: the condition-driven while, the collection-driven .each, and a handful of numeric iteration helpers (times, upto, downto). Pick the one whose shape matches the loop you have.

while GitHub issue

vibecode
{"vibecode": {
    "section": "while",
    "shape": "while_condition_body_end",
    "semantics": "evaluate_condition_before_each_iteration; loop_while_truthy"
}}

while repeats its body while the condition expression is truthy. The condition is re-evaluated before each iteration.

while $foo
    # body
end

No do keyword between the condition and the body — control structures own their body directly. See caspian.md § When do is Required for the rule.

.each GitHub issue

vibecode
{"vibecode": {
    "section": "each",
    "shape": "collection.each(loop_var) do ... end",
    "semantics": "bind_loop_var_to_each_element_in_turn"
}}

Collections provide .each to iterate over their elements:

$items.each($item) do
    # body — $item is bound to each element in turn
end

.each is a method call with a block argument, so it requires do.

Numeric iteration helpers GitHub issue

vibecode
{"vibecode": {
    "section": "numeric_helpers",
    "methods": ["times", "upto", "downto"],
    "returns": "nil",
    "index_base_for_times": "zero_based",
    "upto_downto_inclusive": true
}}

Number values expose three iteration helpers. None of them return a useful value; their job is the side effect of running the block.

Method Description
times Execute the block n times. The block parameter is the 0-based index. 3.times do($i); ...; end
upto($n) Iterate from the current value up to $n inclusive.
downto($n) Iterate from the current value down to $n inclusive.

Examples below use puts, which writes its argument to stdout followed by a newline — distinct from print, which writes the exact string with no trailing newline. Both are stdlib bwcs; pick whichever matches the output shape you want.

5.times do($i)
    puts $i        # 0 1 2 3 4 (one per line)
end

1.upto(3) do($n)
    puts $n        # 1 2 3 (one per line)
end

These are sugar over the underlying iteration machinery — internally they behave the same as .each over the corresponding range and accept as $loop the same way.


Naming a loop with as GitHub issue

vibecode
{"vibecode": {
    "section": "naming_with_as",
    "binds": "loop_object",
    "scope_default": "loop_block",
    "to_retain_after_loop": "pre_declare_variable_in_outer_scope",
    "placement_rule": "as_immediately_follows_the_block_declaration_not_the_receiver",
    "unifying_principle": "see_caspian_md_the_as_keyword_for_the_general_block_handle_rule"
}}

Any of the three loop forms can be named with as to bind a loop object for the duration of the loop. as $name always appears immediately after the block declaration:

$bar.each($foo) as $loop
    puts $loop.count   # current iteration, 1-based
    puts $loop.active?  # true while loop is running
end

By default, the loop object is scoped to the loop block. To retain it after the loop, pre-declare the variable in the outer scope.

For while, as follows the condition (the body opens implicitly):

while($foo) as $loop
    # $loop available inside the body
end

For numeric helpers that take an explicit do(...) block, as follows the do(...) — the block declaration — not the receiver:

5.times do($i) as $loop
    # both $i (index from times) and $loop (loop object) available
end

This placement is the general rule: as modifies the block, not the receiver. In the loop case, the caller (the loop machinery) hands you a rich loop object; in a plain method call, the caller hands you a thin closure-style handle. Same syntax, different richness — see caspian.md § The as Keyword for the unified rule.

Loop object methods GitHub issue

vibecode
{"vibecode": {
    "section": "loop_object_methods",
    "control_methods": ["next", "return", "break"],
    "aliases": {"return": "break"},
    "state_readers": ["count", "active", "index"]
}}
Method Description
$loop.return Exit the loop. Optional value: $loop.return value exits with that value; $loop.return with no argument exits with no value.
$loop.break Alias for $loop.return. Same behavior, same optional value form.
$loop.next Skip to the next iteration
$loop.count Current iteration number (1-based); total count after loop ends
$loop.active? true while the loop is running, false after it ends
$loop.index Current iteration index (0-based)

$loop.return and $loop.break are two names for the same operation — exit this loop. Both accept the optional value form (.return value or .break value); both leave $loop.active? false and $loop.count equal to the iterations that ran. Pick whichever name reads better in context. For the prefix-free top-level form (no $loop reference needed, multi-level exit), see break.

return (without $loop.) is a function exit, not a loop exit. A bare return inside a loop body returns from the enclosing function. There's no special mechanism — return works the same inside or outside a loop. Use $loop.return (or $loop.break) when you want to exit just the loop; use return when you want to return from the enclosing function.


break GitHub issue

vibecode
{"vibecode": {
    "section": "break_bwc",
    "form": "bwc",
    "shape": "[{\"bwc\": \"break\"}, {\"value\": N}?]",
    "arg": "optional_positive_integer_level_count_default_1",
    "function_boundary": "does_not_escape_user_defined_functions_or_closures_named_via_dollar",
    "block_boundary": "DOES_escape_through_do_end_blocks_passed_to_method_calls_per_user_examples",
    "named_loop_targeting_form": "tbd_break_loop_var_as_alternative_to_level_count",
    "history": "added_post_soft_lock_2026-05-17_as_deliberate_v1_addition_per_scotty_section"
}}

break exits a loop without a $loop reference. The plain form exits the innermost enclosing loop; break N exits N enclosing loops.

$people.each do($person)
    break
end

break 2 walks out of two loops at once:

$people.each do($person)
    $person.addresses.each do($address)
        break 2
    end
end

After break N, control resumes after the N-th enclosing loop. The intervening loop objects' $loop.active? becomes false and their $loop.count reflects the iterations that actually ran.

Function boundary GitHub issue

break does not escape function boundaries. If a function definition encloses a loop and contains break, that break exits the loop inside the function; it does not affect any loop in the caller.

function &process_each($list) do
    $list.each do($item)
        break          # exits the .each inside process_each
    end
end

$outer.each do($x)
    &process_each($outer)   # break inside process_each does NOT
                            # exit this .each
end

break does flow through do ... end blocks passed as arguments to methods like .each, .times, .upto. Those are blocks, not function definitions — they execute in the caller's lexical context. The first example above relies on this.

Argument validation GitHub issue

Interaction with structural blocks GitHub issue

If break (or break N) exits a loop, the loop's after structural block does not run — after only runs after a complete iteration sweep. The between block does not run on the iteration that breaks. The noloop block remains a no-op (it only runs when the loop body didn't run at all).

Open question: named-loop targeting GitHub issue

Loops can be named with as $name (see Naming a loop with as). A natural extension is break $outer to target a specific loop by name, which survives refactoring that changes nesting depth. Whether to ship this in V1 alongside break N is TBD. Both can coexist; the level-count form covers the common case and is what was scoped in the post-lock addition.


Structural blocks GitHub issue

vibecode
{"vibecode": {
    "section": "structural_blocks",
    "blocks": ["before", "between", "after", "noloop"],
    "access_to_iteration_variable": false,
    "notes": ["structural_blocks_are_optional;
        each_runs_at_a_defined_phase_of_the_loop"]
}}

Loops support four optional structural blocks. None of them have access to the iteration variable — they exist at the loop's structural phases, not inside the iteration.

$bar.each($foo)
    puts $foo.result
before
    puts "--- START ------"
between
    puts "----------------"
after
    puts "--- END --------"
noloop
    puts "--- NO RESULTS -"
end
Block When it runs
before Once before the first iteration
between Once between each iteration (not before the first, not after the last)
after Once after the last iteration
noloop Only when the collection is empty (no iterations ran)

The before / between / after blocks run unconditionally whenever the body would run at least once. noloop runs exactly when the loop body would not run at all — useful for "nothing matched" messages without an extra emptiness check around the loop.


Deliberately out of scope GitHub issue

vibecode
{"vibecode": {
    "not_in_caspian": ["for_in_form_iteration_is_each_only",
        "redo_retry_no_iteration_restart_construct",
        "outer_function_return_uses_plain_return_no_special_construct"]
}}

These were considered and explicitly excluded:


© 2026 Puck.uno