Skip to content

Commit 9e980db

Browse files
author
Leo Louvar
committed
Fix 4 runtime issues: REPL persistence, parser hang, engine errors, pipe operator
Made-with: Cursor
1 parent 17d58dd commit 9e980db

7 files changed

Lines changed: 124 additions & 51 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,14 @@ flowchart TB
112112
| **Part 2: Language Fundamentals (6–12)** | Variables and all data types (Int, Float, Bool, String, Arrays, Maps), map indexing, type conversions, built-in functions, exercises |
113113
| **Part 3: Control Flow (13–17)** | If/else, while and for loops, pattern matching, decision-making patterns |
114114
| **Part 4: Functions (18–22)** | Definition and calling, parameters vs arguments, lambdas, recursion, scope and best practices |
115-
| **Part 5: Power Features (23–28)** | 25+ built-in functions, engine operations (22 Zig NIFs), Python integration, performance |
115+
| **Part 5: Power Features (23–28)** | 25+ built-in functions, pipe operator (`\|>`), engine operations (22 Zig NIFs), Python integration, performance |
116116
| **Part 6: Real-World Projects (29–35)** | Data pipeline, AI text analysis, LLM integration, workflow automation |
117117
| **Part 7: Mastery (36–40)** | Best practices, performance tips, debugging guide, patterns and anti-patterns |
118118
| **Appendices** | Grammar reference, quick reference card, common patterns, engine operations table |
119119

120120
**Key features:** Progressive learning, 20+ complete examples, 4 real projects, exercises with solutions, visual aids, error-handling focus, AI/automation emphasis.
121121

122-
**Guide stats:** 40+ pages · 20+ code examples · 4 projects · 6 exercises per chapter · 25+ built-in functions · 22 engine ops · modulo, map indexing, type conversions · full grammar reference.
122+
**Guide stats:** 40+ pages · 20+ code examples · 4 projects · 6 exercises per chapter · 25+ built-in functions · 22 engine ops · pipe operator, modulo, map indexing, type conversions · full grammar reference.
123123

124124
**PDF:** A PDF copy is available at [docs/Zixir Language complete guide.pdf](docs/Zixir%20Language%20complete%20guide.pdf) (same path as the guide, .pdf for download). Website: [zixir-lang.github.io/Zixir/Zixir%20Language%20complete%20guide.pdf](https://zixir-lang.github.io/Zixir/Zixir%20Language%20complete%20guide.pdf). To rebuild locally: **Node**`npx md-to-pdf "docs/Zixir Language complete guide.md"` (output is already named with spaces); **or** **pandoc**`./scripts/build-guide-pdf.sh` (Unix) or `.\scripts\build-guide-pdf.ps1` (Windows; requires [pandoc](https://pandoc.org) and LaTeX). To enable automatic PDF build on push: create `.github/workflows/build-guide-pdf.yml` from [scripts/build-guide-pdf-workflow.yml](scripts/build-guide-pdf-workflow.yml) (skip the first 3 comment lines).
125125

@@ -233,8 +233,8 @@ After Setup, run `mix zixir.run examples/hello.zixir`. Expected: `11.0`. For JIT
233233

234234
| Feature | Status | Notes |
235235
|---------|--------|-------|
236-
| Parser | Complete | Recursive descent; tokenization, expressions, control flow, modulo (`%`), map indexing |
237-
| Interpreter | Complete | 25+ built-in functions (`length`, `print`, `to_string`, `type_of`, `split`, `join`, `range`, `reverse`, `abs`, `min`, `max`, etc.), map bracket access, closures |
236+
| Parser | Complete | Recursive descent; tokenization, expressions, control flow, modulo (`%`), map indexing, pipe operator (`\|>`) |
237+
| Interpreter | Complete | 25+ built-in functions, pipe operator (`\|>`), map bracket access, closures, graceful engine error handling |
238238
| Engine NIFs | Complete | 20+ Zig operations (sum, product, dot, etc.) |
239239
| Zig Backend | Complete | Codegen, functions, optimization passes |
240240
| Type System | Complete | Inference, lambda/map/struct types |
@@ -245,7 +245,7 @@ After Setup, run `mix zixir.run examples/hello.zixir`. Expected: `11.0`. For JIT
245245
| Workflow | Complete | Steps, retries, checkpoints, sandboxing |
246246
| Observability | Complete | Logging, metrics, tracing, alerts |
247247
| Cache | Complete | ETS + disk caching |
248-
| CLI/REPL | Working | All commands functional |
248+
| CLI/REPL | Complete | Variable persistence, function persistence, all commands functional |
249249
| Portable CLI | Working | `zixir_run.sh` / `zixir_run.bat` from release; run from any path |
250250
| LSP Server | ✅ Ready | `mix zixir.lsp` + VS Code integration |
251251
| Package Manager | Complete | `Zixir.Package`: resolve, install (Git/path), list, cache; `zixir.toml` manifest |

docs/Zixir Language complete guide.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,16 @@ Before we begin, it's important to understand Zixir's current capabilities:
9595
- ✅ Functions (simple, recursive, closures, lambdas)
9696
- ✅ Pattern matching
9797
- ✅ If/else expressions
98+
- ✅ Pipe operator (`|>`) for chaining function calls
9899
- ✅ Engine operations (22 high-performance Zig NIF operations)
99100
- ✅ Python FFI (module-level functions)
100101

101102
**Current Limitations:**
102103
- ⚠️ **Loops:** While/for loops work but don't support variable accumulation across iterations (by design — Zixir is immutable). Use recursion or engine operations instead.
103104
- ⚠️ **Python Methods:** Only module-level functions work (e.g., `math.sqrt`), not object methods (e.g., `str.lower`)
104-
- ⚠️ **Pipe operator:** `|>` syntax is not yet functional
105105

106106
**Recommended Approach:**
107+
- Use **pipe operator** (`|>`) to chain transformations cleanly
107108
- Use **built-in functions** for common operations (`length`, `to_string`, `print`, `split`, `join`, etc.)
108109
- Use **map indexing** for key-value access (`user["name"]`)
109110
- Use **engine operations** for bulk data processing (fastest)
@@ -728,6 +729,7 @@ Just like in math, some operations happen before others:
728729
6. Equality: ==, !=
729730
7. Logical AND: &&
730731
8. Logical OR: ||
732+
9. Pipe: |> (lowest precedence)
731733
```
732734

733735
```zixir
@@ -888,6 +890,56 @@ let bigger = max(5, 3) # 5
888890

889891
---
890892

893+
## 6c. Pipe Operator
894+
895+
The pipe operator `|>` passes the result of the left expression as the **first argument** to the function on the right. This lets you chain transformations in a readable, top-to-bottom style instead of nesting function calls.
896+
897+
### Basic Usage
898+
899+
```zixir
900+
# Without pipe (nested, reads inside-out)
901+
upper(trim(" hello "))
902+
903+
# With pipe (reads left-to-right)
904+
" hello " |> trim() |> upper() # "HELLO"
905+
```
906+
907+
### Piping into Functions with Arguments
908+
909+
When the right-side function takes additional arguments, the piped value becomes the first argument:
910+
911+
```zixir
912+
fn add(a, b): a + b
913+
10 |> add(5) # 15 (same as add(10, 5))
914+
915+
"hello world" |> contains("world") # true
916+
```
917+
918+
### Chaining Multiple Pipes
919+
920+
```zixir
921+
# Clean and transform a string
922+
" Hello World " |> trim() |> lower() |> length() # 11
923+
924+
# Convert and format
925+
-42 |> abs() |> to_string() # "42"
926+
```
927+
928+
### Pipe with Built-in Functions
929+
930+
All 25+ built-in functions work with pipes:
931+
932+
```zixir
933+
[3, 1, 2] |> reverse() # [2, 1, 3]
934+
[1, 2, 3] |> length() # 3
935+
42 |> type_of() # "Int"
936+
"abc" |> upper() # "ABC"
937+
```
938+
939+
> **Tip:** The pipe operator has the lowest precedence, so `3 + 4 |> to_string()` evaluates `3 + 4` first, then pipes `7` into `to_string()`.
940+
941+
---
942+
891943
## 7. Control Flow
892944

893945
Control flow lets your program make decisions and repeat actions.
@@ -2390,6 +2442,10 @@ map["key"]
23902442
# Field access
23912443
object.field
23922444
2445+
# Pipe operator
2446+
expression |> function_name()
2447+
expression |> function_name(extra_arg)
2448+
23932449
# Parentheses
23942450
(expression)
23952451
```
@@ -2409,6 +2465,7 @@ object.field
24092465
| `==` `!=` | 6 | Equality |
24102466
| `&&` | 7 | Logical AND |
24112467
| `||` | 8 | Logical OR |
2468+
| `\|>` | 9 | Pipe (lowest precedence) |
24122469

24132470
### Function Definition
24142471

@@ -2480,6 +2537,7 @@ const PI = 3.14159 # Compile-time constant
24802537
# Other
24812538
++ # String/array concatenation
24822539
[] # Array/map indexing
2540+
|> # Pipe (chains function calls)
24832541
```
24842542

24852543
### Built-in Functions
@@ -2570,7 +2628,7 @@ python "random" "random" ()
25702628

25712629
### Common Patterns
25722630

2573-
**Note:** Loops with index are not supported in Zixir v1.0. Use engine operations instead.
2631+
**Note:** Loops don't support variable accumulation (Zixir is immutable). Use recursion, pipes, or engine operations instead.
25742632

25752633
**Filter array (using engine):**
25762634
```zixir
14 KB
Binary file not shown.

docs/index.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ <h3>Grammar in brief</h3>
184184
<li><strong>Expressions</strong> — literals (numbers, strings, lists, maps), variables, binary ops (<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>%</code>)</li>
185185
<li><strong>Maps &amp; indexing</strong><code>user["name"]</code> bracket access, <code>keys(map)</code>, <code>values(map)</code></li>
186186
<li><strong>Built-in functions</strong><code>length</code>, <code>print</code>, <code>to_string</code>, <code>type_of</code>, <code>split</code>, <code>join</code>, <code>range</code>, <code>reverse</code>, <code>abs</code>, <code>min</code>, <code>max</code>, and more</li>
187+
<li><strong>Pipe operator</strong><code>value |> transform() |> format()</code> chain functions left-to-right</li>
187188
<li><strong>engine.</strong><code>op(args)</code> — call into the Zig engine (e.g. <code>engine.list_sum([1.0, 2.0, 3.0])</code>)</li>
188189
<li><strong>python</strong> <code>"module" "function" (args)</code> — call Python (e.g. <code>python "math" "sqrt" (4.0)</code>)</li>
189190
<li><strong>match</strong> — pattern matching on values</li>
@@ -192,10 +193,11 @@ <h3>How it runs</h3>
192193
<p style="color: var(--zixir-text-muted); font-size: 0.95rem;">Source is parsed in Elixir into a Zixir AST. It can be <strong>interpreted</strong> (<code>Zixir.eval(source)</code> — engine calls run in Zig NIFs, python calls go to Python via a port) or <strong>compiled</strong> (<code>Zixir.Compiler.compile(source)</code> — type-checks, optimizes, emits Zig; then compile to a native binary or run JIT). Your code stays Zixir-only; the runtime is Elixir + Zig + Python.</p>
193194
<div class="landing-code" style="margin-top: 1rem;">let data = [1.0, 2.0, 3.0, 4.0, 5.0]
194195
let total = engine.list_sum(data)
195-
let count = length(data)
196+
let count = data |> length()
197+
let label = total |> to_string() |> upper()
196198
let user = {"name": "Alice", "score": total}
197-
print("User: " ++ user["name"] ++ ", items: " ++ to_string(count))
198-
# result: "User: Alice, items: 5"</div>
199+
print("User: " ++ user["name"] ++ ", total: " ++ label)
200+
# result: "User: Alice, total: 15.0"</div>
199201
<p style="margin-top: 0.75rem; font-size: 0.9rem;"><a href="LANGUAGE.md" style="color: var(--zixir-purple-400);">Full language spec → LANGUAGE.md</a></p>
200202
</section>
201203

@@ -217,7 +219,7 @@ <h3>What's inside</h3>
217219
<h3>Key features</h3>
218220
<p style="color: var(--zixir-text-muted); font-size: 0.95rem;">Progressive learning · Every concept explained · 20+ complete examples · 4 real projects · Exercises with solutions · Visual aids · Error handling · AI-focused automation workflows.</p>
219221
<h3>Guide statistics</h3>
220-
<p style="color: var(--zixir-text-muted); font-size: 0.95rem;">40+ pages · 20+ code examples · 4 major projects · 6 practice exercises per chapter · 25+ built-in functions · 22 engine operations · Modulo, map indexing, type conversions · Complete grammar reference · Quick reference card.</p>
222+
<p style="color: var(--zixir-text-muted); font-size: 0.95rem;">40+ pages · 20+ code examples · 4 major projects · 6 practice exercises per chapter · 25+ built-in functions · 22 engine operations · Pipe operator, modulo, map indexing, type conversions · Complete grammar reference · Quick reference card.</p>
221223
<p style="margin-top: 1rem;"><a href="https://github.com/Zixir-lang/Zixir/blob/master/docs/Zixir%20Language%20complete%20guide.md" style="color: var(--zixir-purple-400); font-weight: 600;">→ Zixir Language complete guide (read on GitHub)</a> · <a href="https://github.com/Zixir-lang/Zixir/raw/master/docs/Zixir%20Language%20complete%20guide.md" style="color: var(--zixir-purple-400); font-weight: 600;">Download (Markdown)</a> · <a href="Zixir%20Language%20complete%20guide.pdf" style="color: var(--zixir-purple-400); font-weight: 600;">Download PDF</a></p>
222224
<p style="font-size: 0.85rem; color: var(--zixir-text-muted);">To rebuild the PDF locally: <code>npx md-to-pdf "docs/Zixir Language complete guide.md"</code> then keep the generated <code>Zixir Language complete guide.pdf</code>; or use <code>.\scripts\build-guide-pdf.ps1</code> (pandoc + LaTeX).</p>
223225
</section>

lib/zixir/interpreter.ex

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,21 @@ defmodule Zixir.Interpreter do
149149
end
150150

151151
defp eval_expr({:engine_call, func_name, args, _line, _col}, env) do
152-
# Engine call: engine.list_sum([...])
153152
with {:ok, evaled_args} <- eval_args(args, env) do
154153
op = String.to_atom(func_name)
155-
result = Zixir.Engine.run(op, evaled_args)
156-
{:ok, result}
154+
try do
155+
result = Zixir.Engine.run(op, evaled_args)
156+
{:ok, result}
157+
rescue
158+
e in ArgumentError ->
159+
{:error, "Engine operation #{func_name} failed: #{Exception.message(e)}"}
160+
e in ArithmeticError ->
161+
{:error, "Engine operation #{func_name} failed: #{Exception.message(e)}"}
162+
_e in FunctionClauseError ->
163+
{:error, "Engine operation #{func_name}: invalid arguments #{inspect(evaled_args)}"}
164+
e ->
165+
{:error, "Engine operation #{func_name} failed: #{Exception.message(e)}"}
166+
end
157167
end
158168
end
159169

@@ -245,18 +255,20 @@ defmodule Zixir.Interpreter do
245255
end
246256

247257
defp eval_expr({:pipe, left_expr, right_expr}, env) do
248-
# Pipe operator: left |> right
249258
with {:ok, left_val} <- eval_expr(left_expr, env) do
250-
# Inject left value as first argument to right expression
251259
case right_expr do
260+
{:call, func, args, line, col} ->
261+
eval_expr({:call, func, [{:literal, left_val} | args], line, col}, env)
252262
{:call, func, args} ->
253-
eval_expr({:call, func, [left_val | args]}, env)
263+
eval_expr({:call, func, [{:literal, left_val} | args]}, env)
254264
_ ->
255265
{:error, "Right side of pipe must be a function call"}
256266
end
257267
end
258268
end
259269

270+
defp eval_expr({:literal, value}, _env), do: {:ok, value}
271+
260272
defp eval_expr({:async, expr, _line, _col}, env) do
261273
# For now, async just evaluates synchronously
262274
# In a full implementation, this would spawn a Task

lib/zixir/parser/unified.ex

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ defmodule Zixir.Parser.Unified do
102102
"->" => :arrow,
103103
"=>" => :fat_arrow,
104104
".." => :range_op,
105+
"|>" => :|>,
105106
"<" => :lt,
106107
">" => :gt
107108
}
@@ -402,9 +403,15 @@ defmodule Zixir.Parser.Unified do
402403
defp parse_block(tokens), do: {nil, tokens}
403404

404405
defp parse_block_contents([{:op, :"}", _, _} | rest], acc), do: {{:block, Enum.reverse(acc)}, rest}
405-
406+
407+
defp parse_block_contents([], _acc) do
408+
raise ParseError, message: "Unexpected end of input, expected '}' to close block", line: 0, column: 0
409+
end
410+
406411
defp parse_block_contents(tokens, acc) do
407412
case parse_statement(tokens) do
413+
{nil, []} ->
414+
raise ParseError, message: "Unexpected end of input, expected '}' to close block", line: 0, column: 0
408415
{nil, rest} -> parse_block_contents(rest, acc)
409416
{stmt, rest} -> parse_block_contents(rest, [stmt | acc])
410417
end
@@ -500,7 +507,23 @@ defmodule Zixir.Parser.Unified do
500507
# Parser - Expressions (with operator precedence)
501508
# ============================================================================
502509

503-
defp parse_expression(tokens), do: parse_or(tokens)
510+
defp parse_expression(tokens), do: parse_pipe(tokens)
511+
512+
defp parse_pipe(tokens) do
513+
{left, rest} = parse_or(tokens)
514+
case rest do
515+
[{:op, :|>, _, _} | rest2] ->
516+
{right, rest3} = parse_or(rest2)
517+
parse_pipe_chain({:pipe, left, right}, rest3)
518+
_ -> {left, rest}
519+
end
520+
end
521+
522+
defp parse_pipe_chain(left, [{:op, :|>, _, _} | rest]) do
523+
{right, rest2} = parse_or(rest)
524+
parse_pipe_chain({:pipe, left, right}, rest2)
525+
end
526+
defp parse_pipe_chain(left, rest), do: {left, rest}
504527

505528
defp parse_or(tokens) do
506529
{left, rest} = parse_and(tokens)
@@ -755,11 +778,6 @@ defmodule Zixir.Parser.Unified do
755778
parse_identifier_suffix({:field, expr, field}, rest)
756779
end
757780

758-
defp parse_identifier_suffix(expr, [{:op, :|>, _, _} | rest]) do
759-
{right, rest2} = parse_unary(rest)
760-
parse_identifier_suffix({:pipe, expr, right}, rest2)
761-
end
762-
763781
defp parse_identifier_suffix(expr, rest), do: {expr, rest}
764782

765783
defp parse_arg_list(tokens), do: parse_arg_list_impl(tokens, [])

lib/zixir/repl.ex

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ defmodule Zixir.REPL do
3131
require Logger
3232

3333
@welcome_message """
34-
Welcome to Zixir REPL v0.1.0
34+
Welcome to Zixir REPL v7.1.0
3535
Type :help for help, :quit to exit
3636
3737
"""
@@ -194,45 +194,28 @@ defmodule Zixir.REPL do
194194
end
195195

196196
defp evaluate(code, state) do
197-
case Zixir.eval(code) do
198-
{:ok, result} ->
199-
# Extract any new variable bindings from the code
200-
new_env = extract_bindings(code, result, state.env)
201-
202-
# Print result (unless it's nil from a let statement)
197+
case Zixir.Interpreter.eval(code, state.env) do
198+
{:ok, result, new_env} ->
203199
if result != nil do
204200
IO.puts(format_result(result))
205201
end
206-
202+
207203
{:continue, %{state | env: new_env, history: [code | state.history]}}
208-
204+
205+
{:error, message} when is_binary(message) ->
206+
IO.puts("Error: #{message}")
207+
{:continue, %{state | multiline_buffer: "", line_count: 0}}
208+
209209
{:error, %Zixir.CompileError{} = error} ->
210210
IO.puts("Error: #{error.message}")
211-
if error.line && error.line > 0 do
212-
IO.puts(" at line #{error.line}, column #{error.column}")
213-
end
214211
{:continue, %{state | multiline_buffer: "", line_count: 0}}
215-
212+
216213
{:error, reason} ->
217214
IO.puts("Error: #{inspect(reason)}")
218215
{:continue, %{state | multiline_buffer: "", line_count: 0}}
219216
end
220217
end
221218

222-
defp extract_bindings(code, _result, env) do
223-
# Simple extraction of let bindings
224-
# In a full implementation, we'd parse and track all bindings properly
225-
case Regex.run(~r/^let\s+(\w+)\s*=/, code) do
226-
[_, var_name] ->
227-
# Re-evaluate to get the value
228-
case Zixir.eval(code) do
229-
{:ok, value} -> Map.put(env, var_name, value)
230-
_ -> env
231-
end
232-
_ -> env
233-
end
234-
end
235-
236219
defp format_result(result) when is_float(result) do
237220
# Format floats nicely
238221
if result == trunc(result) do

0 commit comments

Comments
 (0)