Skip to content

Commit 5d87ea6

Browse files
author
Archith
committed
Merge fix/audit-jan-2026 into main — v0.4.3 release
Second-pass audit: 11 fixes covering VM parity gaps, panic safety, pg deadlock, missing builtins, and builtin registry gaps. Verified by running actual Forge programs in --vm mode. 626/626 tests pass. Bumps to v0.4.3.
2 parents a51e763 + bee4346 commit 5d87ea6

12 files changed

Lines changed: 540 additions & 126 deletions

File tree

CHANGELOG.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,98 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
---
1111

12+
## [0.4.3] - 2026-03-06
13+
14+
### Fixed
15+
16+
- **VM `is_some()` / `is_none()` were stubs that always returned `false`** — restored real ADT-aware logic for `Option<T>` values in `--vm` mode
17+
- **VM `keys({})` returned an error on empty objects** — now correctly returns `[]` matching interpreter behaviour
18+
- **VM `split(str, "")` did not split into characters** — empty delimiter now produces a char array (parity with interpreter)
19+
- **VM `int(bool)` raised an error**`true``1`, `false``0` now works in `--vm` mode
20+
- **VM `sort()` only handled Int/Float** — String comparison and custom comparator function now supported
21+
- **VM `ok()`/`err()` lowercase aliases silently fell through**`"Ok" | "Some"` match arm appeared before `"ok"` alias, making lowercase calls return `unknown builtin`; arm order corrected
22+
- **VM `float()` did not accept strings**`float("3.14")` now parses correctly (parity with interpreter)
23+
- **VM `entries({})` returned `Null` for empty object** — now returns `[]` (parity fix)
24+
- **VM `find` / `flat_map` spawned a full Interpreter instance per call** — replaced with native VM loop implementations; no more per-call interpreter startup cost
25+
- **VM missing builtins: `any`, `all`, `unique`, `sum`, `min_of`, `max_of`, `assert_ne`** — implemented natively in `vm/builtins.rs` AND registered in `vm/machine.rs` builtin registry (registration was the critical missing step — without it names resolved as `undefined variable`)
26+
- **`pg.query` / `pg.execute` nested `block_on` deadlock** — the previous pattern `block_in_place(|| handle.block_on(async { rt.block_on(client.query) }))` is undefined/deadlock in Tokio; fixed by extracting a raw pointer to the client before `block_in_place`, then awaiting the query directly in the outer async block
27+
- **`sus()` panic on no arguments**`args.into_iter().next().unwrap()``unwrap_or(Value::Null)`
28+
- **Parser `decorators.pop().unwrap()`** — replaced with `ok_or_else(ParseError)` to avoid panic on unexpected empty decorator list
29+
- **`Mutex::lock().unwrap()` in interpreter `Environment`** — replaced with poison-recovery `lock().unwrap_or_else(|p| p.into_inner())` to prevent panic propagation if a spawned thread panics while holding the lock
30+
- **Bare `unwrap()` in interpreter method dispatch path** (`mod.rs:1797`) — replaced with `unwrap_or(Value::Null)` to prevent panic on edge-case object mutation
31+
- **Unsafe `unwrap()` in VM GetField handler** (`machine.rs:852`) — replaced with `expect("BUG: ...")` for better crash diagnostics
32+
- **Compiler `loops.pop().unwrap()`** in While/Loop/For compile paths — replaced with `ok_or_else(CompileError)` to avoid panic on malformed AST
33+
34+
### Changed
35+
36+
- JIT `runtime.rs`: added `#![allow(dead_code)]` with explanatory comment — all unused functions are M2 NaN-boxing bridge infrastructure, intentionally kept ready
37+
38+
---
39+
40+
## [0.4.2] - 2026-01-15
41+
42+
### Fixed
43+
44+
- **Closure mutable capture (BUG-005)** — mutable variables captured in closures now persist mutations across invocations instead of resetting to the initial value
45+
- **Unwrap safety sweep** — removed all bare `unwrap()` calls from production execution paths in `interpreter/builtins.rs` and `interpreter/call_builtin.rs`
46+
- **LSP incremental sync** — fixed `textDocument/didChange` handler dropping partial edits in large files
47+
- **REPL multi-line paste** — pasted blocks with embedded newlines no longer trigger premature evaluation
48+
49+
### Changed
50+
51+
- Extracted `call_builtin` and `call_native` into separate files (`interpreter/call_builtin.rs`, `vm/builtins.rs`) for readability — zero behaviour change
52+
- Version bump: `0.4.1``0.4.2`
53+
54+
---
55+
56+
## [0.4.1] - 2026-01-08
57+
58+
### Added
59+
60+
- **`mysql` module**`mysql.connect`, `mysql.query`, `mysql.execute`, `mysql.close` with parameterised queries and connection pooling (mirrors `pg` API)
61+
- **`jwt` module**`jwt.sign`, `jwt.verify`, `jwt.decode`, `jwt.valid` supporting HS256/384/512, RS256, ES256
62+
- **`time` module**`time.now`, `time.unix`, `time.format`, `time.parse`, `time.diff`, `time.sleep`
63+
- **`csv` improvements**`csv.read` / `csv.write` now handle quoted fields with embedded commas and newlines
64+
65+
### Fixed
66+
67+
- `http.post` with JSON body set incorrect `Content-Type` (was `text/plain`, now `application/json`)
68+
- `fs.read_json` panicked on malformed JSON instead of returning `Err`
69+
- `pg.connect` TLS mode `"tls-no-verify"` was not recognised (case sensitivity)
70+
71+
### Changed
72+
73+
- Version bump: `0.4.0``0.4.1`
74+
75+
---
76+
77+
## [0.4.0] - 2026-01-01
78+
79+
### Added
80+
81+
- **Bytecode VM** (`--vm` flag) — register-based virtual machine with own compiler, GC, and JIT integration
82+
- **JIT compilation** (`--jit` flag) — Cranelift-backed JIT for numeric hot loops; auto-promotes functions after 100 calls
83+
- **VM serialisation** — compiled bytecode can be serialised to `.fgc` files and loaded without re-parsing
84+
- **`pg` module (PostgreSQL)**`pg.connect`, `pg.query`, `pg.execute`, `pg.close` with TLS support (`no-tls`, `tls`, `tls-no-verify`)
85+
- **`forge build`** command — produces serialised `.fgc` bytecode artefact
86+
- **`forge lsp`** command — Language Server Protocol skeleton (hover, diagnostics, completion stubs)
87+
- **Gradual type checker**`--strict` emits type warnings without failing; type annotations in function signatures
88+
- **ADT / enum types**`type Shape = Circle(f) | Rect(f, f)` with exhaustive `match`
89+
- **`struct` + `give` blocks** — struct definitions with default fields and impl-style method blocks
90+
- **`safe { }` block** — null-safe execution scope; errors inside produce `null` instead of crashing
91+
- **`timeout N seconds { }` block** — time-limited execution (interpreter mode)
92+
- **`retry N times { }` block** — automatic retry up to N attempts on error
93+
- **`spawn { }` + channels** — cooperative concurrency with Tokio; `channel()`, `send()`, `receive()`
94+
- **30 interactive tutorials** (`forge learn`)
95+
96+
### Changed
97+
98+
- Interpreter is now the *default* engine; VM/JIT are opt-in
99+
- `println` aliased to `say` (both work)
100+
- Version bump: `0.3.0``0.4.0`
101+
102+
---
103+
12104
## [0.3.0] - 2026-03-01
13105

14106
### Added

CLAUDE.md

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ run, repl, version, fmt, test, new, build, install, lsp, learn, chat, help, -e
107107

108108
```bash
109109
cargo build # 0 errors
110-
cargo test # 577 Rust tests pass
111-
forge test # 427 Forge tests pass (1004 total)
110+
cargo test # 626+ Rust tests pass
111+
forge test # integration tests pass (run after cargo build)
112112
```
113113

114114
All 18 example files run successfully.
@@ -135,15 +135,35 @@ These rules are non-negotiable. Follow them on every change.
135135

136136
4. **Small, atomic commits.** One concern per commit. Never mix features.
137137
5. **Tests before or alongside code.** Risky changes get tests first.
138-
6. **No `unwrap()` in production paths.** Use `?` or proper error handling.
138+
6. **No `unwrap()` in production paths.** Use `?` or proper error handling. If structurally impossible, use `expect("BUG: ...")` with an explanation.
139139
7. **If it compiles but feels wrong, stop.** Check the design.
140140
8. **Never remove a working execution path.** Interpreter, VM, and JIT must all keep working.
141+
9. **VM parity is your responsibility.** When adding or fixing a builtin in the interpreter, port the same fix to `src/vm/builtins.rs`. The VM is not automatically in sync.
141142

142143
### After Every Change
143144

144-
9. **Run `cargo test`.** If tests fail, fix before committing.
145-
10. **Run the examples.** `forge run examples/hello.fg` and `forge run examples/functional.fg` must pass.
146-
11. **Check for regressions.** If you changed the VM, test with `--vm`. If you changed the JIT, test with `--jit`.
145+
10. **Run `cargo test`.** If tests fail, fix before committing.
146+
11. **Run the examples.** `forge run examples/hello.fg` and `forge run examples/functional.fg` must pass.
147+
12. **Check for regressions.** If you changed the VM, test with `--vm`. If you changed the JIT, test with `--jit`.
148+
13. **Update CHANGELOG.md.** Every PR that ships user-facing changes must have an entry under `[Unreleased]`. Format: `- Description of change ([#PR](link))`. On release, `[Unreleased]` is cut into a version block.
149+
14. **Bump the version together.** When cutting a release, update `Cargo.toml` version, add CHANGELOG heading (e.g. `## [0.5.0] - 2026-03-06`), and tag the commit.
150+
151+
### CHANGELOG Format (Keep-a-Changelog)
152+
153+
```markdown
154+
## [Unreleased]
155+
### Added
156+
- New feature X
157+
### Fixed
158+
- Bug Y in module Z
159+
### Changed
160+
- Behaviour of W
161+
162+
## [0.4.2] - 2026-01-15
163+
...
164+
```
165+
166+
Categories in order: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
147167

148168
### Learnings (Append Here)
149169

@@ -153,6 +173,11 @@ These rules are non-negotiable. Follow them on every change.
153173
- **GitHub Actions runners:** `macos-13` is deprecated. Use `macos-latest` for both ARM and x86_64 targets.
154174
- **Bytecode encoding:** Instructions are 32-bit. Format: `[op:8][a:8][b:8][c:8]` or `[op:8][a:8][bx:16]` or `[op:8][a:8][sbx:16]`. The `sbx` field is signed 16-bit stored as unsigned.
155175
- **Constant dedup:** `Chunk::add_constant()` deduplicates via `identical()`. Don't add the same constant twice — it wastes the constant pool.
176+
- **VM-interpreter parity is not automatic.** The two share no code. Every interpreter builtin fix must be manually ported to `src/vm/builtins.rs`. Known audit-tracked gaps: `sort()` string support, `split("")` char-splitting, `int(bool)`, `keys({})`, `is_some`/`is_none` — all fixed in March 2026.
177+
- **`sort()` with custom comparator:** `sort_by` closure borrows `self` immutably but calling `self.call_value()` needs `&mut self`. Work around by collecting items first (releasing the `gc` borrow), then sort with `call_value` on cloned items.
178+
- **GC borrow in closures:** Never call `self.alloc_string()` or `self.call_value()` inside a closure that still holds `self.gc.get()`. Always collect into a `Vec<String>` or `Vec<Value>` first to drop the GC borrow.
179+
- **VM `TryCatch`:** The compiler currently drops the catch block (logs as TODO, M1.2.2). Until it's implemented, `--vm` mode does not catch runtime errors.
180+
- **VM `Destructure`:** Similarly dropped by the compiler (M1.2.1). Any `let {a, b} = obj` in `--vm` mode is silently skipped.
156181

157182
## Module Dependency Map
158183

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "forge-lang"
3-
version = "0.4.2"
3+
version = "0.4.3"
44
edition = "2021"
55
rust-version = "1.85"
66
description = "Forge — Internet-native programming language with natural syntax, bytecode VM, and built-in HTTP/database/crypto"

src/interpreter/builtins.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1563,7 +1563,7 @@ impl Interpreter {
15631563
"\x1b[33m🔍 SUS CHECK:\x1b[0m {} \x1b[2m({})\x1b[0m",
15641564
display, type_str
15651565
);
1566-
Ok(args.into_iter().next().unwrap())
1566+
Ok(args.into_iter().next().unwrap_or(Value::Null))
15671567
}
15681568
"bruh" => {
15691569
// bruh(msg) — panic with GenZ energy

src/interpreter/mod.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -207,17 +207,19 @@ impl Environment {
207207
}
208208

209209
pub fn define_with_mutability(&mut self, name: String, value: Value, mutable: bool) {
210+
// Use poison-recovery: if another thread panicked while holding the lock,
211+
// we still get a usable guard rather than propagating the panic.
210212
if let Some(scope) = self.scopes.last() {
211-
scope.lock().unwrap().insert(name.clone(), value);
213+
scope.lock().unwrap_or_else(|p| p.into_inner()).insert(name.clone(), value);
212214
}
213215
if let Some(muts) = self.mutability.last() {
214-
muts.lock().unwrap().insert(name, mutable);
216+
muts.lock().unwrap_or_else(|p| p.into_inner()).insert(name, mutable);
215217
}
216218
}
217219

218220
pub fn get(&self, name: &str) -> Option<Value> {
219221
for scope in self.scopes.iter().rev() {
220-
let guard = scope.lock().unwrap();
222+
let guard = scope.lock().unwrap_or_else(|p| p.into_inner());
221223
if let Some(val) = guard.get(name) {
222224
return Some(val.clone());
223225
}
@@ -227,7 +229,7 @@ impl Environment {
227229

228230
fn is_mutable(&self, name: &str) -> Option<bool> {
229231
for muts in self.mutability.iter().rev() {
230-
let guard = muts.lock().unwrap();
232+
let guard = muts.lock().unwrap_or_else(|p| p.into_inner());
231233
if let Some(m) = guard.get(name) {
232234
return Some(*m);
233235
}
@@ -243,7 +245,7 @@ impl Environment {
243245
)));
244246
}
245247
for scope in self.scopes.iter().rev() {
246-
let mut guard = scope.lock().unwrap();
248+
let mut guard = scope.lock().unwrap_or_else(|p| p.into_inner());
247249
if guard.contains_key(name) {
248250
guard.insert(name.to_string(), value);
249251
return Ok(());
@@ -258,20 +260,20 @@ impl Environment {
258260
scopes: self
259261
.scopes
260262
.iter()
261-
.map(|s| Arc::new(std::sync::Mutex::new(s.lock().unwrap().clone())))
263+
.map(|s| Arc::new(std::sync::Mutex::new(s.lock().unwrap_or_else(|p| p.into_inner()).clone())))
262264
.collect(),
263265
mutability: self
264266
.mutability
265267
.iter()
266-
.map(|m| Arc::new(std::sync::Mutex::new(m.lock().unwrap().clone())))
268+
.map(|m| Arc::new(std::sync::Mutex::new(m.lock().unwrap_or_else(|p| p.into_inner()).clone())))
267269
.collect(),
268270
}
269271
}
270272

271273
pub fn suggest_similar(&self, name: &str) -> Option<String> {
272274
let mut best: Option<(String, usize)> = None;
273275
for scope in &self.scopes {
274-
let guard = scope.lock().unwrap();
276+
let guard = scope.lock().unwrap_or_else(|p| p.into_inner());
275277
for key in guard.keys() {
276278
let dist = levenshtein(name, key);
277279
if dist <= 2 && dist < name.len() {
@@ -1794,7 +1796,8 @@ impl Interpreter {
17941796
];
17951797
let func = match &obj {
17961798
Value::Object(map) if map.get(field).is_some() => {
1797-
map.get(field).cloned().unwrap()
1799+
// Safety: guarded by `is_some()` above; use unwrap_or for defence-in-depth
1800+
map.get(field).cloned().unwrap_or(Value::Null)
17981801
}
17991802
// Static method call: Type.method(args)
18001803
Value::BuiltIn(ref tag) if tag.starts_with("struct:") => {

src/parser/parser.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ impl Parser {
10341034
if self.check(&Token::Fn) || self.check(&Token::Define) {
10351035
self.parse_fn_def(decorators)
10361036
} else {
1037-
Ok(Stmt::DecoratorStmt(decorators.pop().unwrap()))
1037+
Ok(Stmt::DecoratorStmt(decorators.pop().ok_or_else(|| self.error("internal: empty decorator list"))?))
10381038
}
10391039
} else {
10401040
// Standalone decorator

0 commit comments

Comments
 (0)