Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ jobs:
- name: Build (Release)
run: cargo build --release --verbose

# Concurrency Phase 0 (PR-0a) gate: prove the panic-strategy gate is live.
# The `#[cfg(panic = "abort")] compile_error!` in src/lib.rs must fail the
# build when panic=abort is forced (via --config so Cargo actually compiles
# the crate with -C panic=abort). If this build SUCCEEDS the gate is not
# enforced and catch_unwind fault isolation would be a phantom control.
- name: Assert panic=abort is rejected
run: |
set +e
output=$(cargo build --release --config 'profile.release.panic="abort"' --quiet 2>&1)
status=$?
set -e
# The gate must fail the build *with our specific compile_error*, not
# merely error out for some unrelated reason.
if [ "$status" -eq 0 ] || ! grep -Fq 'WFL requires panic = "unwind"' <<<"$output"; then
printf '%s\n' "$output"
echo "::error::panic=abort was not rejected by the src/lib.rs compile_error gate"
exit 1
fi
echo "panic=abort correctly rejected by the compile_error gate"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Run tests (integration tests now have access to release binary)
- name: Run Tests
run: cargo test --verbose
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ Source Code → Lexer → Parser → Analyzer → Type Checker → Interpreter
- A working example (in `TestPrograms/`, validated with MCP) demonstrating the feature.
- A Dev Diary entry in `Dev diary/` for any non-trivial feature or behavior change.
- When a feature is removed or its syntax changes, remove or fix the now-stale docs and examples — don't leave contradictions.
- **Docs Must Be Honest — "validate docs" (MANDATORY)**: Documentation describes **what actually ships today**, not what is aspirational. This is a binding policy, not a preference:
- **No overclaiming runtime behavior.** Never describe behavior the runtime does not have (e.g. calling serial request handlers "parallel" or saying they "don't block others"). Prefer the precise word — say "concurrent" (interleaved on one thread) vs "parallel" (multiple cores) deliberately, and describe the transport/handler split accurately.
- **Mark planned/future behavior explicitly.** Anything not yet implemented must be labeled as planned/future so a reader never mistakes it for current behavior.
- **Validate, don't just assert.** Every user-visible change ships **validated** docs (MCP tools + `python scripts/validate_docs_examples.py` for any touched example) **and** a Dev Diary entry, in the **same change**. "Validate docs" means both: the examples run, and the prose matches the implementation.
- When behavior changes, fix the now-stale claims in the same change — a doc that contradicts the code is a bug.
- **Location**: `Docs/` organized in 6 sections (Introduction, Getting Started, Language Basics, Advanced Features, Standard Library, Best Practices).
- **Structure**: Follow `Docs/wfl-documentation-policy.md` and 19 principles in `Docs/wfl-foundation.md`.
- **Reference Documentation**: Two-tiered system for keywords
Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@ harness = false

[profile.release]
debug = true
# Concurrency Phase 0 (PR-0a): pin unwinding so the runtime's catch_unwind-based
# request-handler fault isolation (Phase 1) stays sound. Enforced by the
# `#[cfg(panic = "abort")] compile_error!` in src/lib.rs.
panic = "unwind"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
122 changes: 122 additions & 0 deletions Dev diary/2026-07-11-phase-0-concurrency-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Dev Diary — 2026-07-11: Phase 0 concurrency hardening

## Context

`Docs/development/concurrency-phase-plan.md` lays out a phased plan to give WFL a
real concurrency model without a risky interpreter rewrite. **Phase 0 ("Kill the
live DoS")** is the first, lowest-risk slice: no interpreter-core redesign, no new
language surface, plain `main loop` stays serial. It closes three concrete gaps
that a public WFL web server exposes today:

1. **CPU-bound crypto stalls the whole process.** Password hashing (Argon2,
scrypt, bcrypt, PBKDF2 at 600k rounds) is *deliberately* slow and ran inline on
the single interpreter thread, so one login-like call froze every other request
— a cooperative-scheduling DoS.
2. **The request queue was unbounded.** The transport→interpreter channel was an
`mpsc::unbounded_channel`, so a flood of accepted requests could grow memory
without bound.
3. **The docs claimed parallelism we don't have.** Several pages said handlers run
"concurrently" / "don't block others" while they actually run one at a time.

This entry covers all three sub-PRs (0a/0b/0c) that landed together on
`claude/phase-0-implementation-rpbdjp`.

## What changed

### 0a — Docs honesty + `panic = "unwind"` gate

- **Panic gate.** The runtime will rely on `catch_unwind` to contain a panicking
request handler so its siblings survive (Phase 1). That is unsound under
`panic = "abort"`. Enforced with `#[cfg(panic = "abort")] compile_error!(...)`
in `src/lib.rs` — evaluated with the crate's *actual* panic strategy, so it
fails a real abort build but never trips `cargo test` (Cargo force-unwinds test
harnesses).
- **Dead end worth recording:** the first attempt used a `build.rs` check on the
`CARGO_CFG_PANIC` env var. It does not work — build scripts run on the host
and always see `unwind`, even under `--config 'profile.release.panic="abort"'`
(verified empirically). A `#[test]` on `cfg!(panic = ...)` is likewise a
phantom control. The crate-level `compile_error!` is the only mechanism that
reflects the real target panic strategy.
- `Cargo.toml` `[profile.release]` now pins `panic = "unwind"` explicitly
(self-documenting; default was already unwind).
- `.github/workflows/ci.yml` `clippy-and-test` job gains an "Assert panic=abort
is rejected" step that forces abort via `--config` and **fails if the build
succeeds** — the failing-first control proving the gate is live.
- **Docs.** Rewrote overclaims to distinguish the concurrent *transport* (accept /
TLS) from *serial application handlers*, and preferred "concurrent" over
"parallel". Primary rewrite in `async-programming.md`; `web-servers.md` was
already honest and served as the model.

### 0b — `spawn_blocking` for CPU-heavy crypto

- New `src/stdlib/crypto_async.rs` with a name-keyed `route(name, args)` that hops
the 11 heavy crypto builtins onto Tokio's blocking pool. Chosen over a new
`Value::AsyncNativeFunction` variant to avoid rippling a new arm through every
exhaustive `Value` match for zero user-visible benefit.
- The interpreter's two async native-dispatch arms (`FunctionCall`, `ActionCall`
in `src/interpreter/mod.rs`) now check `route()` first and `.await` it, falling
back to the synchronous native otherwise.
- The heavy compute stayed in plain-data helpers in `src/stdlib/crypto.rs`
(`argon2_hash_str`, `*_verify_str`, `pbkdf2_hmac_sha256_str`, …), now
`pub(crate)`. Arguments are extracted into owned `String`/`u64`/`usize` on the
interpreter thread *before* the hop; only that plain data crosses into
`spawn_blocking`; the `Value` is rebuilt after the `.await`.
- **Interpreter core stays `!Send`.** No `Rc`/`RefCell`/`Value`/`Environment` ever
crosses a thread boundary (HARD RULE 9 not triggered). `zeroize` and the
`subtle` constant-time compare paths are untouched — no early-exit refactor.

### 0c — Bounded request queue (OOM shed)

- New `.wflcfg` key `web_server_request_queue_bound` (default `256`, zero
rejected) in `src/config.rs`.
- The transport→interpreter channel is now `mpsc::channel(bound)` instead of
`unbounded_channel`. The warp handler uses `try_send`: on `Full` it logs a
structured warning and returns a `503` (with `Retry-After`) via the new
`overloaded_response()` helper, without blocking the transport task or awaiting
the per-request oneshot; on `Closed` it keeps the existing rejection.
- WebSocket channels remain unbounded — out of scope, noted as a follow-up.

## Tests

- `src/stdlib/crypto_async.rs` unit tests (in-crate because `route` is
`pub(crate)`): exact routed-set map; a **deterministic off-thread proof** — on a
`current_thread` runtime a concurrently-spawned ticker only advances during the
crypto `.await` if the work was offloaded (independent of core count, no timing
thresholds); routed hash/verify round-trips (argon2, bcrypt); routed PBKDF2 is
byte-identical to the direct helper; argument errors still surface.
- Existing crypto suites (`crypto_kdf_test`, `password_hashing_test`,
`crypto_test` — 44 tests) pass unchanged through the new routed dispatch path.
- `src/interpreter/mod.rs` `queue_bound_tests`: `overloaded_response()` is a
well-formed 503 (Content-Length matches body); a full bounded channel sheds
deterministically via `try_send` → `Full`.
- `tests/web_queue_bound_test.rs`: `web_server_request_queue_bound` parsing —
default 256, valid override applied, zero and non-numeric rejected.
- Existing web-server tests (`http_request_runtime_test`, `respond_headers_test`,
`route_params_test`, `websocket_test` — 30 tests) pass with the bounded queue at
the default.

## Docs

- `Docs/04-advanced-features/async-programming.md`, `index.md`,
`Docs/01-introduction/key-features.md`,
`Docs/06-best-practices/performance-tips.md`, `Docs/Archive/README.md`,
`Docs/04-advanced-features/web-servers.md` — honesty pass (concurrent transport
vs serial handlers; concurrent ≠ parallel).
- `Docs/reference/configuration-reference.md` and `web-servers.md` — document
`web_server_request_queue_bound` and the 503 shed behavior.
- `Docs/development/concurrency-phase-plan.md` — flipped 0a/0b/0c to ✅.
- `CLAUDE.md` — added a **"Docs must be honest (validate docs)"** policy: docs
describe what actually ships (planned behavior marked as such), and every
user-visible change ships validated docs *and* a Dev Diary entry in the same
change.

## Notes / Follow-ups

- Phase 0 is cooperative only: CPU-bound *non-awaiting* WFL code still stalls the
process. Concurrent request handlers arrive in Phase 1 (`main loop concurrently:`).
- The crypto auto-call dispatch path (arity-0 natives) is intentionally not
routed; every heavy crypto builtin takes ≥ 1 argument. A future arity-0 heavy
builtin would also need routing.
- WebSocket outbound/event channels are still unbounded — a Phase 1 follow-up.
- `#![deny(clippy::await_holding_refcell_ref)]` already guards `src/lib.rs`, which
is the clippy backstop Phase 1 will lean on.
2 changes: 1 addition & 1 deletion Docs/01-introduction/key-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ close file input_file
display "File contents: " with file_data
```

Non-blocking I/O is natural and easy to use.
Cooperative, non-blocking I/O is natural and easy to use.

## 4. Built-in Web Server

Expand Down
35 changes: 23 additions & 12 deletions Docs/04-advanced-features/async-programming.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Async Programming

WFL supports asynchronous operations using natural language syntax. Handle multiple operations concurrently without blocking.
WFL supports asynchronous operations using natural language syntax. The `wait for` keyword lets a slow operation yield cooperatively — while it waits on I/O, the WFL runtime can make progress on other awaited work instead of the thread sitting idle.

## What is Async?

**Synchronous (blocking):** Operations run one at a time. If one is slow, everything waits.
**Synchronous (blocking):** Operations run one at a time, and while one runs the thread can do nothing else.

**Asynchronous (non-blocking):** Operations can run concurrently. Slow operations don't block others.
**Asynchronous (cooperative):** An awaited operation *yields* while it waits on I/O, so the runtime can drive other awaited work in the meantime.

> **Concurrent, not parallel.** WFL's async today is cooperative and single-threaded: awaited work is *interleaved* on one thread, not run on multiple cores at once. In a plain script, statements — including `wait for` statements — still execute one after another. The payoff of `wait for` is that a waiting operation releases the thread to the runtime rather than hard-blocking it. Running independent operations so they actually overlap is a planned feature (see [Concurrent Async](#concurrent-async-future-feature) below).

## The `wait for` Keyword

Expand Down Expand Up @@ -56,7 +58,7 @@ close file file2
// Total time: Time1 + Time2
```

### With Async (Non-Blocking)
### With Async (Cooperative)

```wfl
// Prepare two sample files
Expand All @@ -67,16 +69,20 @@ open file at "file2.txt" for writing as setup2
wait for write content "second file" into setup2
close file setup2

// Operations can overlap
// Each `wait for` still completes before the next statement runs — these do
// not overlap. What `wait for` changes is that while an operation waits on
// I/O, the thread yields to the runtime instead of hard-blocking, so other
// runtime work (such as a web server's transport layer) keeps making progress.
open file at "file1.txt" for reading as file1
wait for store content1 as read content from file1 // Doesn't block
wait for store content1 as read content from file1 // Yields while waiting
close file file1

open file at "file2.txt" for reading as file2
wait for store content2 as read content from file2 // Can run concurrently
wait for store content2 as read content from file2 // Runs after the first
close file file2

// Total time: ~max(Time1, Time2)
// Total time today: Time1 + Time2. Overlapping independent operations is a
// planned feature (see "Concurrent Async" below) — it is not available yet.
```

## Common Async Operations
Expand All @@ -102,7 +108,8 @@ display "File read complete"
```wfl
listen on port 8080 as web_server

// Async request handling: wait for a request without blocking other work
// Wait for the next request. The transport layer accepts connections
// concurrently, but your handler code below runs one request at a time.
wait for request comes in on web_server as incoming

respond to incoming with "Response" and content_type "text/plain"
Expand Down Expand Up @@ -158,7 +165,10 @@ end check

## Async in Web Servers

Web servers naturally use async operations:
Web servers naturally use async operations. Note that request *handlers* run one
at a time today — the transport layer (accepting connections, TLS handshakes) is
concurrent, but your handler code is serial. See
[Web Servers → Limitations](web-servers.md#limitations--notes) for details.

```wfl
listen on port 8081 as web_server
Expand Down Expand Up @@ -202,7 +212,8 @@ display "All operations complete"

### Concurrent Async (Future Feature)

Planned syntax for running operations in parallel:
Planned syntax for running independent operations concurrently (so they actually
overlap instead of running one after another):

```wfl
// This is planned for future versions
Expand Down Expand Up @@ -317,7 +328,7 @@ WFL's async support is built on the Tokio runtime and includes:
In this section, you learned:

✅ **The `wait for` keyword** - Async operation syntax
✅ **Why async matters** - Non-blocking operations
✅ **Why async matters** - Cooperative, non-blocking I/O (concurrent, not parallel)
✅ **Common async operations** - File I/O, web requests, directory listing
✅ **Error handling** - Try-catch with async
✅ **Async in web servers** - Request handling
Expand Down
4 changes: 2 additions & 2 deletions Docs/04-advanced-features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ If you've completed those, you're ready for advanced features!
These features are "advanced" not because they're complicated, but because they're **powerful**:

- **Web Servers** - Build HTTP APIs and web applications
- **Async** - Handle multiple operations concurrently
- **Async** - Cooperative, non-blocking I/O with `wait for`
- **File I/O** - Persist data and process files
- **Pattern Matching** - Validate and extract data
- **Containers** - Organize code with object-oriented programming
Expand Down Expand Up @@ -130,7 +130,7 @@ Object-oriented programming with readable syntax.
**Focus on:**
1. [Containers (OOP)](containers-oop.md) - Code organization
2. [Web Servers](web-servers.md) - Backend services
3. [Async Programming](async-programming.md) - Concurrent operations
3. [Async Programming](async-programming.md) - Cooperative async I/O
4. [File I/O](file-io.md) - Persistence

## Real-World Examples
Expand Down
3 changes: 2 additions & 1 deletion Docs/04-advanced-features/web-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,7 @@ end check

- **Single request handling:** Each `wait for request` handles one request
- **Blocking:** Server handles requests sequentially (TLS handshakes are concurrent, but your responses are serialized)
- **Bounded accept queue:** Because handlers are serial, incoming requests queue up behind the one being handled. That queue is bounded (default 256, configurable via `web_server_request_queue_bound`). When it is full, the server sheds new requests with a `503 Service Unavailable` (plus a `Retry-After` header) and logs a warning, rather than growing memory without bound. See [Configuration Reference](../reference/configuration-reference.md#web_server_request_queue_bound).
- **No middleware system** (yet) - Implement manually
- **No built-in session management** - Implement yourself

Expand Down Expand Up @@ -1196,7 +1197,7 @@ Expand your web development skills:
Learn to read and write files for data persistence.

**[Async Programming →](async-programming.md)**
Handle multiple operations concurrently.
Cooperative async I/O with `wait for`.

**[Pattern Matching →](pattern-matching.md)**
Validate request data and extract parameters.
Expand Down
4 changes: 2 additions & 2 deletions Docs/06-best-practices/performance-tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ display file_content
// wait for request comes in on web_server as req
```

**Why:** Non-blocking I/O lets WFL handle other work while waiting.
**Why:** While an awaited I/O operation is pending, WFL yields the thread to the runtime instead of hard-blocking it, so runtime-level work keeps progressing. (Request handlers still run one at a time — see the web server docs.)

## Short-Circuit Evaluation

Expand Down Expand Up @@ -368,7 +368,7 @@ end action
## Best Practices

✅ **Choose right algorithm** - Most important!
✅ **Use async for I/O** - Non-blocking operations
✅ **Use async for I/O** - Cooperative, non-blocking operations
✅ **Cache expensive results** - Don't recalculate
✅ **Compile patterns once** - Reuse across iterations
✅ **Short-circuit cleverly** - Cheap checks first
Expand Down
2 changes: 1 addition & 1 deletion Docs/Archive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ end count
## ✨ Key Features

- **📖 Natural Language Syntax**: Write code that reads like English sentences
- **🚀 Modern Async Support**: Built-in async/await for concurrent operations
- **🚀 Modern Async Support**: Built-in async/await for cooperative (single-thread) concurrency
- **🛡️ Type Safety**: Static type checking with intelligent inference
- **🌐 Web-First Design**: Native HTTP and database support
- **🌍 Built-in Web Server**: Create HTTP servers with `listen on port 8080` - no external frameworks needed
Expand Down
6 changes: 3 additions & 3 deletions Docs/development/concurrency-phase-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ HARD RULES:

| Phase | PR | Title | Status |
|-------|-----|--------|--------|
| 0 | 0a | Docs honesty + `panic=unwind` CI | ⬜ Not started |
| 0 | 0b | `spawn_blocking` for blocking crypto | ⬜ Not started |
| 0 | 0c | Bound accept/queue (OOM shed) | ⬜ Not started |
| 0 | 0a | Docs honesty + `panic=unwind` CI | ✅ Done |
| 0 | 0b | `spawn_blocking` for blocking crypto | ✅ Done |
| 0 | 0c | Bound accept/queue (OOM shed) | ✅ Done |
| 1 | 1a | Runtime spike (bridge, no surface) | ⬜ Not started |
| 1 | 1b | `main loop concurrently:` surface + ops defaults | ⬜ Not started |
| 1 | 1c | Honesty docs for real concurrent model | ⬜ Not started |
Expand Down
Loading
Loading