Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1fee337
feat: add shared ExecutionBudget consolidating runtime resource caps
claude Jul 12, 2026
953fcd6
fix: address PR review — budget spans execute-file, nested source cap…
claude Jul 12, 2026
db3fff0
fix(test): use forward-slash paths in execute-file budget tests (Wind…
claude Jul 12, 2026
6dcf0b7
fix(P1): one budget per run + bounded source loader for every entry p…
claude Jul 12, 2026
e93e40b
fix(P1-1): dedicated RAII recursion-depth counter; don't corrupt stat…
claude Jul 12, 2026
ccf2179
fix(P1-2): meter pattern transitions per-instruction on one shared bu…
claude Jul 12, 2026
9dd9315
fix(P2): register budget keys with ConfigChecker; document real MSRV
claude Jul 12, 2026
fc7012f
fix(P1-3): stream HTTP body, global request guard + timeout, reliable…
claude Jul 12, 2026
a4a28ba
docs: record deep-review round (P1-1..P1-5 + P2) in Dev Diary
claude Jul 12, 2026
1319b31
fix(P1): per-match pattern meter with deadline sampling; keep VM API
claude Jul 12, 2026
7d67693
fix(P1): bound HTTP request lifetime; prune abandoned requests
claude Jul 12, 2026
c198984
fix(P1): bound WebSocket queued bytes; cancellable close handshake
claude Jul 12, 2026
de801d3
fix(P1): share recursion depth across execute-file boundary
claude Jul 12, 2026
4e58e9c
fix(P2): config checker validates budget keys against loader ranges
claude Jul 12, 2026
d1eaba1
fix(P1/P2): REPL enforces source cap pre-lex; per-command budget + Ct…
claude Jul 12, 2026
d852ea8
fix(P1): thread the budget through the front end
claude Jul 12, 2026
24e61d5
docs: document round-2 budget changes (WS bytes, request deadline, fr…
claude Jul 12, 2026
89e9b71
fix: address round-2 bot review (deadline exemption, yield, checker)
claude Jul 12, 2026
4eb1f8c
fix(P1): main-loop exemption as a shared depth counter + RAII guard
claude Jul 12, 2026
80781e2
fix(P1): harden WebSocket + HTTP lifecycle and peak allocation
claude Jul 12, 2026
c1c8826
fix(P1): scope budget across REPL/front end; fatal type-check budget …
claude Jul 12, 2026
d22d109
docs: record round-3 review (main-loop depth, WS/HTTP lifecycle, scop…
claude Jul 12, 2026
4e9ad3c
fix: poll the execution budget inside recursive front-end traversal
claude Jul 12, 2026
8cfda12
fix: make a type-check budget breach a fatal typed result, not a side…
claude Jul 12, 2026
013024a
perf: collect pattern input once, checkpoint before preprocessing
claude Jul 12, 2026
7e798b7
fix: hold the HTTP admission slot until the response completes
claude Jul 12, 2026
0cc0559
fix: guarantee a WebSocket Disconnect for every admitted Connect
claude Jul 12, 2026
9aab926
fix: scope the run budget task-locally so interleaved runs cannot cro…
claude Jul 12, 2026
81ccad5
fix: budget-checkpoint the lexer and recursive expression parsing/ana…
claude Jul 12, 2026
28e05b4
fix: expose the large interpreter stack as a public helper for embedders
claude Jul 12, 2026
5bf878e
docs: Dev Diary entry for the fifth review round (maintainer P1s)
claude Jul 12, 2026
8bd70bb
fix: address issue #611 — lexer typed outcome, HTTP admission release…
claude Jul 12, 2026
285d2e0
fix: consult the run budget at the lexer boundary, not only on a full…
claude Jul 12, 2026
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ Source Code → Lexer → Parser → Analyzer → Type Checker → Interpreter
- **Rules**: Refer to `.cursor/rules/wfl-rules.mdc`.

## Technical Requirements
- **Rust Edition**: 2024 (Min: 1.75+, Dev: 1.91.1+)
- **Rust Edition**: 2024 (MSRV: 1.88+ — the codebase uses `let`-chains; Dev: 1.91.1+)
- **Versioning**: YY.MM.BUILD (e.g., 26.1.22). Major version always < 256 (Windows MSI compatibility).
- **Key Dependencies**:
- `logos`: Lexer
Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
name = "wfl"
version = "26.7.35"
edition = "2024"
# Minimum supported Rust version. The codebase uses `if let … && …` let-chains
# (stabilized in Rust 1.88) throughout, so 1.88 is the true floor — recorded
# here so `cargo` fails fast on older toolchains instead of deep in a build.
rust-version = "1.88"
description = "WFL (WebFirst Language) is a programming language designed to be readable and intuitive using natural language constructs."
license = "Apache-2.0"
authors = ["Logbie LLC <info@logbie.com>"]
Expand Down
465 changes: 465 additions & 0 deletions Dev diary/2026-07-12-shared-execution-budget.md

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions Docs/04-advanced-features/web-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1027,9 +1027,14 @@ 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).
- **Bounded response size:** A handler cannot `respond with` a body larger than `web_server_max_response_size` (default 64 MiB); an oversized response is refused with a runtime error rather than streamed unbounded. See [Configuration Reference](../reference/configuration-reference.md#web_server_max_response_size).
- **Bounded request body (chunked-safe):** The request-body limit (`web_server_max_body_size`) is enforced *while the body streams in*, so a chunked upload with no `Content-Length` is bounded too — an oversized body is refused with `413 Payload Too Large` without being fully buffered.
- **Global in-flight cap + request deadline:** The accepted-request cap is shared across every `listen` server via one budget, and one deadline (`web_server_response_timeout_seconds`, default 300s) is set at admission and covers the whole accepted-request lifetime. A body that is not fully received in time is shed with `408 Request Timeout` (so a slow "trickle" upload under the size cap cannot pin a slot), and a handler that does not answer in time is shed with `504 Gateway Timeout`. A shed or abandoned request is skipped and its bookkeeping pruned rather than run as zombie work.
- **No middleware system** (yet) - Implement manually
- **No built-in session management** - Implement yourself

All of these ceilings, together with the request timeout and body-size limits, are part of one shared [execution budget](../reference/configuration-reference.md#execution-budget-resource-limits).

### Workarounds

**For multiple requests:** Use loops (requires signal handling for shutdown)
Expand Down Expand Up @@ -1159,6 +1164,33 @@ wait for 3600 seconds
close server chat_server
```

### WebSocket resource limits

WebSocket queues and connections are bounded — by **count and by bytes** — so a
flood or a slow client cannot grow memory without bound:

- **Queue bound** (`web_socket_queue_bound`, default 1024): the per-connection
outbound frame queue and the per-server event queue are bounded by frame
*count*. When a queue is full, the extra frame/event is dropped and a warning
is logged.
- **Per-message size** (`web_socket_max_message_size`, default 1 MiB): a single
inbound or outbound text frame larger than this is dropped (with a warning)
rather than queued, so the count bound above also bounds each frame's size.
- **Global queued bytes** (`web_socket_max_queued_bytes`, default 16 MiB): every
queued payload reserves its byte length against one global ceiling and releases
it when the frame is delivered, consumed, or shed — bounding total buffered
WebSocket memory across all connections at once.
- **Connection limit** (`web_socket_max_connections`, default 1024): a
connection attempt beyond the limit is refused with a close frame and a logged
warning.

`close server` also terminates each live connection deterministically: it signals
every connection to stop reading (so a peer that ignores the close handshake
cannot keep a socket task or its connection slot alive), sends a close frame, and
tears the socket down after a bounded close-handshake timeout.

All of these are part of the shared [execution budget](../reference/configuration-reference.md#execution-budget-resource-limits).

## Security Considerations

⚠️ **Important:** Web servers expose your application to the internet. Always:
Expand Down
150 changes: 149 additions & 1 deletion Docs/reference/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,35 @@ All keys currently loaded from config files, with defaults.
| `web_server_bind_address` | IP string | `127.0.0.1` | Bind address for `listen on port` |
| `web_server_tls_cert_file` | path | *(none)* | Default PEM cert for bare `listen … secured` |
| `web_server_tls_key_file` | path | *(none)* | Default PEM key for bare `listen … secured` |
| `web_server_max_body_size` | integer ≥ 1 | `1048576` (1 MiB) | Max HTTP request body size (bytes) |
| `web_server_max_body_size` | integer ≥ 1 | `1048576` (1 MiB) | Max HTTP request body size (bytes); enforced while streaming (chunked-safe) |
| `web_server_max_response_size` | integer ≥ 1 | `67108864` (64 MiB) | Max HTTP response body size (bytes) |
| `web_server_request_queue_bound` | integer ≥ 1 | `256` | Max queued HTTP requests before shedding with 503 |
| `web_server_response_timeout_seconds` | integer ≥ 0 | `300` | Seconds to await a handler before shedding with 504; `0` disables |
| `web_socket_queue_bound` | integer ≥ 1 | `1024` | Max queued frames/events per WebSocket channel before shedding |
| `web_socket_max_connections` | integer ≥ 1 | `1024` | Max simultaneous live WebSocket connections |
| `web_socket_max_message_size` | integer ≥ 1 | `1048576` (1 MiB) | Max size of a single WebSocket text message (bytes); larger frames are dropped |
| `web_socket_max_queued_bytes` | integer ≥ 1 | `16777216` (16 MiB) | Global ceiling on queued WebSocket payload bytes across all connections |

### Execution budget keys (summary)

A single [`ExecutionBudget`](#execution-budget-resource-limits) governs every
resource ceiling as one coherent mechanism. These keys tune it (detailed below).
Each is chosen so ordinary programs never trip it while runaway behavior gets a
clean, catchable error instead of a crash or unbounded memory growth.

| Key | Type | Default | Purpose |
|---|---|---|---|
| `max_operations` | integer ≥ 0 | `0` (unlimited) | Hard ceiling on interpreter operations; `0` disables it |
| `max_call_depth` | integer ≥ 1 | `1000` | Max WFL call/recursion depth |
| `max_import_depth` | integer ≥ 1 | `64` | Max nested `load module` / `include` depth |
| `max_execute_file_depth` | integer ≥ 1 | `4` | Max `execute file` nesting depth |
| `max_pattern_steps` | integer ≥ 1 | `5000000` | Max pattern-matching transitions per match (ReDoS guard) |
| `max_pattern_states` | integer ≥ 1 | `10000` | Max simultaneously-active pattern states per match |
| `max_source_size` | integer ≥ 1 | `67108864` (64 MiB) | Max WFL source-file size (bytes) |

The wall-clock deadline (`timeout_seconds`), request body/response ceilings, and
the HTTP/WebSocket queue and connection bounds above are all part of the same
budget.

---

Expand Down Expand Up @@ -495,6 +522,127 @@ Maximum number of accepted-but-not-yet-handled HTTP requests held in the queue b

Because request handlers run one at a time (see [Web Servers → Limitations](../04-advanced-features/web-servers.md#limitations--notes)), a burst of traffic queues up behind the handler. Without a bound, that queue could grow until the process runs out of memory. When the queue is full, the server **sheds** further requests with a `503 Service Unavailable` (and a `Retry-After` header) and logs a warning, instead of buffering unbounded work. Raise it to absorb larger bursts at the cost of more memory; lower it to shed sooner under load. A value of `0` is rejected (the default is kept).

#### `web_server_max_response_size`

Maximum HTTP response body a handler may `respond with`, in bytes. A larger response is refused (the handler gets a runtime error) rather than streaming an unbounded payload to the client.

- **Type:** Integer (bytes, at least 1)
- **Default:** `67108864` (64 MiB)
- **Example:** `web_server_max_response_size = 5242880` # 5 MiB

#### `web_server_response_timeout_seconds`

Maximum time, in seconds, the transport waits for a handler to answer an accepted request before shedding it with a `504 Gateway Timeout` and freeing its in-flight slot. This bounds a dequeued-but-never-answered request so it cannot pin an in-flight slot indefinitely.

- **Type:** Integer (0 or more)
- **Default:** `300`
- **Example:** `web_server_response_timeout_seconds = 30`

A value of `0` disables the timeout. The in-flight request cap (`web_server_request_queue_bound`) is enforced globally across every `listen` server via one shared budget, and a request's slot is held from the moment its body starts streaming until the handler responds, this timeout fires, or the client disconnects.

#### `web_socket_queue_bound`

Maximum number of queued frames (per outbound connection) and lifecycle events (per server) held for a WebSocket before shedding. Bounds WebSocket memory the same way `web_server_request_queue_bound` bounds HTTP requests: when a channel is full, the extra frame/event is dropped and a warning is logged, instead of growing memory without bound.

- **Type:** Integer (at least 1)
- **Default:** `1024`
- **Example:** `web_socket_queue_bound = 4096`

#### `web_socket_max_connections`

Maximum number of simultaneous live WebSocket connections. A connection attempt beyond the limit is refused (the server sends a close frame and logs a warning) instead of registering unbounded connections.

- **Type:** Integer (at least 1)
- **Default:** `1024`
- **Example:** `web_socket_max_connections = 256`

#### `web_socket_max_message_size`

Maximum size in bytes of a single WebSocket text message, applied to both inbound frames and outbound `send`/`broadcast` frames. A larger frame is dropped (with a warning) rather than queued, so the per-message memory a connection can pin is bounded — the frame-count bound (`web_socket_queue_bound`) alone does not bound the *size* of each queued frame.

- **Type:** Integer (at least 1)
- **Default:** `1048576` (1 MiB)
- **Example:** `web_socket_max_message_size = 262144`

#### `web_socket_max_queued_bytes`

Global ceiling in bytes on all WebSocket payloads queued across every connection's inbound event and outbound frame channels at once. Each queued frame reserves its byte length against this ceiling and releases it when the frame is delivered, consumed, or shed, so a slow or absent consumer cannot buffer WebSocket memory without bound even under the per-message and per-channel count limits.

- **Type:** Integer (at least 1)
- **Default:** `16777216` (16 MiB)
- **Example:** `web_socket_max_queued_bytes = 8388608`

### Execution budget (resource limits)

WFL enforces every resource ceiling through a single shared **execution budget**
object that travels with a run through parsing, evaluation, pattern matching, web
handling, and module loading. Consolidating these caps in one place means they
behave consistently and are tuned from one section of `.wflcfg`. The wall-clock
deadline is `timeout_seconds` (above); the byte and queue ceilings are the
`web_server_*` / `web_socket_*` keys (above). The remaining knobs:

#### `max_operations`

Hard ceiling on the number of interpreter operations a run may execute. This is a belt-and-suspenders guard against a program that spins without ever awaiting (which the wall-clock `timeout_seconds` may not catch promptly inside a tight loop).

- **Type:** Integer (0 or more)
- **Default:** `0` (unlimited — matches historic behavior)
- **Example:** `max_operations = 500000000`

A value of `0` disables the ceiling. Like `timeout_seconds`, this ceiling is **not** enforced inside a `main loop` (a long-lived server would otherwise stop after N operations); cooperative cancellation still applies.

#### `max_call_depth`

Maximum WFL call/recursion depth. When exceeded, the run stops with a clean, catchable *“Maximum call depth (N) exceeded — possible infinite recursion”* error instead of a native stack overflow that would abort the whole process.

- **Type:** Integer (at least 1)
- **Default:** `1000`
- **Example:** `max_call_depth = 2000`

WFL runs the interpreter on a large (1 GiB) stack so this depth is reached safely; if you raise the ceiling substantially and rely on very deep recursion, prefer an iterative formulation where practical.

#### `max_import_depth`

Maximum nesting depth of `load module` / `include from`. Circular imports are already detected separately; this bounds a legitimately deep — but likely accidental — dependency chain.

- **Type:** Integer (at least 1)
- **Default:** `64`
- **Example:** `max_import_depth = 128`

#### `max_execute_file_depth`

Maximum nesting depth of `execute file` runs. Kept small because each level re-enters the whole interpreter recursively.

- **Type:** Integer (at least 1)
- **Default:** `4`
- **Example:** `max_execute_file_depth = 6`

#### `max_pattern_steps`

Maximum number of pattern-VM transitions a single match attempt may take (Regular-expression Denial-of-Service, “ReDoS”, guard). A pathological pattern that would otherwise run away stops with a pattern step-limit error.

- **Type:** Integer (at least 1)
- **Default:** `5000000`
- **Example:** `max_pattern_steps = 250000`

#### `max_pattern_states`

Maximum number of simultaneously-active states a single pattern match may hold. Bounds exponential state fan-out that step-counting alone does not catch.

- **Type:** Integer (at least 1)
- **Default:** `10000`
- **Example:** `max_pattern_states = 50000`

#### `max_source_size`

Maximum size, in bytes, of a WFL source file. A larger file is refused before it is lexed or parsed.

- **Type:** Integer (bytes, at least 1)
- **Default:** `67108864` (64 MiB)
- **Example:** `max_source_size = 1048576` # 1 MiB

Each of these positive-integer keys rejects `0` and non-numeric values, keeping the default and logging a warning.

---

## How config relates to lint, style, and servers
Expand Down
Loading
Loading