Loops in Goblin
You need to do something ten times. Or loop through a list of items. Or keep going until a condition changes.
In Python, you use for for some of these and while for others. In JavaScript, you have for, while, for...in, for...of, and forEach, and you have to remember which one to use when. Ruby gives you for, while, until, loop, each, times, upto, and more—good luck picking the right one.
Goblin has one keyword: repeat.
That's it. No memorizing when to use for versus while. No looking up whether you need for...in or for...of this time. You just write repeat: and give it something—a number, a condition, a collection—and Goblin figures out what you mean.
Want to run code 10 times? repeat: 10. Want to loop while health is above zero? repeat: health > 0. Want to process each item in an array? repeat: items.
One keyword. Every loop.
while(cond, { ... }), for(name, iterable, { ... }), and repeat(count, { ... }). This page describes the upcoming unified repeat: syntax.How Goblin Decides What Kind of Loop You Want
Goblin looks at what you give it after repeat: and decides based on the type:
| You give it... | You get... |
|---|---|
Nothing (repeat:) |
Infinite loop |
A number (10) |
Count loop (runs N times) |
A bool (x > 0) |
Conditional loop (while true) |
| An array | Loop over each item |
| A string | Loop over each character |
A range (3..6) |
Loop over the range |
| A map | Loop over key/value pairs |
| Something else | Error |
No guessing. No coercion. If Goblin doesn't know how to repeat it, it tells you.
Let's see each one.
1. Infinite Loop — Bare repeat:
repeat tick() stop if: done? xx
repeat tick() stop if: done? xx
No expression after repeat: means "run forever." You escape with stop, stop if, or return.
One-liner version:
repeat => tick()
repeat => tick()
2. Count Loop — Give It a Number
repeat 10 :say("digging...") xx
repeat 10 :say("digging...") xx
Runs exactly 10 times. That's it. No for i in range(10) ceremony.
If you need to know which iteration you're on, Goblin gives you idx:
repeat 5 :say("tunnel {idx}") xx
repeat 5 :say("tunnel {idx}") xx
Output:
tunnel 0
tunnel 1
tunnel 2
tunnel 3
tunnel 4
Starts at 0. If you give it 0 or a negative number, the loop doesn't run at all.
One-liner:
repeat 10 => :say("hello")
repeat 10 => :say("hello")
3. Conditional Loop — Give It a Bool
health | 100 repeat health > 0 :say("still alive: {health}") health |= health - 10 xx
health | 100 repeat health > 0 :say("still alive: {health}") health |= health - 10 xx
Output:
still alive: 100
still alive: 90
still alive: 80
...
still alive: 10
The condition is checked before each iteration. If it's false from the start, the loop never runs.
This is Goblin's "while loop" — you just write the condition after repeat:.
4. Item Loops — Arrays, Strings, Ranges
Here's where things get interesting.
loot | ["sword", "shield", "potion"] repeat loot :say("found: {it}") xx
loot | ["sword", "shield", "potion"] repeat loot :say("found: {it}") xx
Output:
found: sword
found: shield
found: potion
Inside the loop, it is the current item. You also get idx:
repeat loot :say("{idx}: {it}") xx
repeat loot :say("{idx}: {it}") xx
Output:
0: sword
1: shield
2: potion
Strings Work Too
repeat "goblin" :say(it) xx
repeat "goblin" :say(it) xx
Output:
g
o
b
b
l
i
n
And Ranges
Goblin has two kinds of ranges:
a..b— exclusive end (doesn't includeb)a...b— inclusive end (includesb)repeat 3..6 :say(it) xx
repeat 3..6 :say(it) xx
Output:
3
4
5
repeat 3...6 :say(it) xx
repeat 3...6 :say(it) xx
Output:
3
4
5
6
Naming Items Yourself
Don't like it? Use as:
heroes | [ { "name": "Grom", "hp": 100 }, { "name": "Zug", "hp": 85 } ] repeat heroes as hero :say("{hero["name"]} has {hero["hp"]} HP") xx
heroes | [ { "name": "Grom", "hp": 100 }, { "name": "Zug", "hp": 85 } ] repeat heroes as hero :say("{hero["name"]} has {hero["hp"]} HP") xx
Output:
Grom has 100 HP
Zug has 85 HP
When you use as, it disappears, but idx still works.
5. Map Loops — Key/Value Pairs
stats | { "str": 10, "dex": 15, "int": 8 } repeat stats :say("{key} => {val}") xx
stats | { "str": 10, "dex": 15, "int": 8 } repeat stats :say("{key} => {val}") xx
Output:
str => 10
dex => 15
int => 8
Or make it readable:
repeat stats :say("{key} has {val} hit points") xx
repeat stats :say("{key} has {val} hit points") xx
Output:
str has 10 hit points
dex has 15 hit points
int has 8 hit points
Explicit binding:
repeat stats as (k, v) :say("{k} = {v}") xx
repeat stats as (k, v) :say("{k} = {v}") xx
6. The Secret: Goblin Manages the Cursor
Here's what makes Goblin different.
In most languages, when you loop over a collection, you're managing an index—even if the language hides it:
Python:
for i in range(len(items)): item = items[i] # You're managing i, even if Python hides it
for i in range(len(items)): item = items[i] # You're managing i, even if Python hides it
JavaScript:
for (let i = 0; i < items.length; i++) { let item = items[i]; // You manage i yourself }
for (let i = 0; i < items.length; i++) { let item = items[i]; // You manage i yourself }
In Goblin, there's a hidden loop cursor that tracks where you are. You never see it. You never manage it. It just works.
You get:
it— current elementidx— current position
And Goblin steps forward automatically each iteration.
You never write:
i | 0 # Never needed i |= i + 1 # Never needed
i | 0 # Never needed i |= i + 1 # Never needed
You never check:
stop if: i >= items.len # Never needed
stop if: i >= items.len # Never needed
Goblin handles all of this.
Unless you want to override it.
7. Slices and Starting Points
You can slice collections before looping:
items | ["a", "b", "c", "d", "e"] repeat items[2..] :say(it) xx
items | ["a", "b", "c", "d", "e"] repeat items[2..] :say(it) xx
Output:
c
d
e
With ranges:
repeat items[1...3] :say(it) xx
repeat items[1...3] :say(it) xx
Output:
b
c
d
8. Loop Control — skip, stop, return
skip — Go to Next Iteration
repeat loot skip if: it == "junk" :say("keeping: {it}") xx
repeat loot skip if: it == "junk" :say("keeping: {it}") xx
When you hit "junk", Goblin skips the rest and moves to the next item.
stop — Break Out
repeat items stop if: it == "bomb" process(it) xx
repeat items stop if: it == "bomb" process(it) xx
Loop ends immediately when you hit "bomb".
return — Exit the Function
act find_first_big(nums) repeat nums if it > 100 => return it xx return nil xx
act find_first_big(nums) repeat nums if it > 100 => return it xx return nil xx
Returns the first number over 100, exits the function entirely.
Sugar: skip if and stop if
These are just shortcuts:
stop if: COND # -> if COND => stop skip if: COND # -> if COND => skip
stop if: COND # -> if COND => stop skip if: COND # -> if COND => skip
Use whichever is clearer.
9. Filtering — "Where" Without a Keyword
A lot of languages have special filtering syntax:
Python:
for item in items: if item["type"] != "post": continue render_post(item)
for item in items: if item["type"] != "post": continue render_post(item)
JavaScript:
items.filter(item => item.type === "post") .forEach(item => renderPost(item));
items.filter(item => item.type === "post") .forEach(item => renderPost(item));
Goblin doesn't need a where or .filter():
repeat items skip if: it["type"] != "post" render_post(it) xx
repeat items skip if: it["type"] != "post" render_post(it) xx
skip if is your filter. Multiple conditions? Stack them:
repeat posts skip if: it["draft"] skip if: it["archived"] skip if: it["deleted"] publish(it) xx
repeat posts skip if: it["draft"] skip if: it["archived"] skip if: it["deleted"] publish(it) xx
Each guard skips items that don't match.
10. Do-While Pattern — Run At Least Once
Some languages have do...while:
C:
do { body(); } while (condition);
do { body(); } while (condition);
Goblin doesn't need it:
repeat body() stop if: not (condition) xx
repeat body() stop if: not (condition) xx
Infinite loop + stop if = do-while behavior. No extra keyword needed.
11. One-Liners
Everything works as a one-liner:
repeat items => process(it) repeat x>0 => tick() repeat => watchdog()
repeat items => process(it) repeat x>0 => tick() repeat => watchdog()
12. Why jump Exists — Manual Hell Before Goblin
Before jump, programmers had to do this garbage:
Manual skip-ahead:
i = 0 while i < len(items): item = items[i] if item == "header": i += 5 # Skip ahead continue process(item) i += 1
i = 0 while i < len(items): item = items[i] if item == "header": i += 5 # Skip ahead continue process(item) i += 1
Manual index. Manual increment. Manual bounds checking.
Manual scan-forward:
i = 0 while i < len(items): item = items[i] if item == "start": # Scan until we find "end" i += 1 while i < len(items) and items[i] != "end": i += 1 continue process(item) i += 1
i = 0 while i < len(items): item = items[i] if item == "start": # Scan until we find "end" i += 1 while i < len(items) and items[i] != "end": i += 1 continue process(item) i += 1
Nested loops. Off-by-one errors. Index hell.
This is what other languages force you to do.
13. jump — Cursor Control Made Simple
jump lets you override the automatic cursor. It only works in item loops (ones with it and idx).
jump N — Skip Ahead
repeat items if it == "header" => jump 5 xx process(it) xx
repeat items if it == "header" => jump 5 xx process(it) xx
Skip ahead exactly 5 items. The cursor moves, the loop continues from there.
jump until COND — Scan Forward
repeat items if it == "start" => jump until it == "end" xx handle(it) xx
repeat items if it == "start" => jump until it == "end" xx handle(it) xx
Scans forward until the condition is true, then continues from there.
Compare to the manual version:
Manual (Python):
i = 0 while i < len(items): item = items[i] if item == "start": i += 1 while i < len(items) and items[i] != "end": i += 1 continue process(item) i += 1
i = 0 while i < len(items): item = items[i] if item == "start": i += 1 while i < len(items) and items[i] != "end": i += 1 continue process(item) i += 1
Goblin:
repeat items if it == "start" => jump until it == "end" process(it) xx
repeat items if it == "start" => jump until it == "end" process(it) xx
No nested loops. No manual index. Just "jump until you find the end."
jump N until COND — Stride-Scan
repeat numbers jump 2 until it == 10 :say("found {it} at idx {idx}") xx
repeat numbers jump 2 until it == 10 :say("found {it} at idx {idx}") xx
Moves forward 2 steps at a time, checking the condition after each step.
14. How jump Works
When you use jump:
- The rest of the current loop body is skipped
- The cursor moves forward according to your command
- The loop continues from the new position
If the cursor goes past the end, the loop ends naturally. No errors.
Jump only works in item loops. Count loops and condition loops don't have a cursor, so jump doesn't make sense there.
Real Example: Parsing a Document
You're parsing tokens where sections are marked with START and END:
Without jump (Other Languages):
i = 0 sections = [] while i < len(tokens): token = tokens[i] if token["type"] == "comment": i += 1 continue if token["type"] == "section_start": section_name = token["name"] content = [] i += 1 while i < len(tokens) and tokens[i]["type"] != "section_end": content.append(tokens[i]) i += 1 sections.append({"name": section_name, "content": content}) i += 1
i = 0 sections = [] while i < len(tokens): token = tokens[i] if token["type"] == "comment": i += 1 continue if token["type"] == "section_start": section_name = token["name"] content = [] i += 1 while i < len(tokens) and tokens[i]["type"] != "section_end": content.append(tokens[i]) i += 1 sections.append({"name": section_name, "content": content}) i += 1
With jump (Goblin):
act parse_document(tokens) sections | [] repeat tokens skip if: it["type"] == "comment" if it["type"] == "section_start" => section_name | it["name"] content | [] repeat jump 1 stop if: idx >= tokens.len stop if: it["type"] == "section_end" :put_last!(content, it) xx :put_last!(sections, { "name": section_name, "content": content }) xx xx return sections xx
act parse_document(tokens) sections | [] repeat tokens skip if: it["type"] == "comment" if it["type"] == "section_start" => section_name | it["name"] content | [] repeat jump 1 stop if: idx >= tokens.len stop if: it["type"] == "section_end" :put_last!(content, it) xx :put_last!(sections, { "name": section_name, "content": content }) xx xx return sections xx
No manual index management. No bounds checking. The complex case looks almost as clean as the simple case.
Putting It All Together
Let's look at a more realistic example. You're building a game loop:
act game_loop() player_hp | 100 repeat command | :read_line() stop if: command == "quit" if command == "heal" player_hp |= player_hp + 20 :say("Healed! HP: {player_hp}") elif command == "damage" player_hp |= player_hp - 15 :say("Ouch! HP: {player_hp}") stop if: player_hp <= 0 else :say("Unknown command: {command}") xx xx :say("Game over!") xx
act game_loop() player_hp | 100 repeat command | :read_line() stop if: command == "quit" if command == "heal" player_hp |= player_hp + 20 :say("Healed! HP: {player_hp}") elif command == "damage" player_hp |= player_hp - 15 :say("Ouch! HP: {player_hp}") stop if: player_hp <= 0 else :say("Unknown command: {command}") xx xx :say("Game over!") xx
This uses:
- An infinite loop (
repeat:with no expression) stop ifto exit when the player quits or dies- Retethering (
|=) to update player HP - String interpolation to show current state
All the pieces working together naturally.
Summary
Goblin has one loop keyword: repeat.
What you give it determines the loop type:
- Nothing → infinite
- Number → count
- Bool → conditional
- Collection → item loop
- Map → key/value loop
Inside item loops:
it/idxare automatic- Hidden cursor managed for you
jumpto override cursor when needed
Control flow:
skip/stop/returnskip if/stop ifas sugar
Patterns:
- Filter with
skip if - Do-while with
repeat:+stop if - Complex scanning with
jump until
No for. No while. No foreach. No manual indices.
Just repeat.