Skip to content

Commit c869929

Browse files
authored
Merge pull request #48 from mgomes/mgomes/v0-17-0
v0.17.0: module ergonomics and hardening
2 parents ca2befc + 97bff54 commit c869929

21 files changed

+1697
-61
lines changed

ROADMAP.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ Goal: improve language ergonomics for complex script logic and recovery behavior
217217

218218
### Error Handling Constructs
219219

220-
- [ ] Add structured error handling syntax (`begin/rescue/ensure` or equivalent).
221-
- [ ] Add typed error matching where feasible.
222-
- [ ] Define re-raise semantics and stack preservation.
220+
- [x] Add structured error handling syntax (`begin/rescue/ensure` or equivalent).
221+
- [x] Add typed error matching where feasible.
222+
- [x] Define re-raise semantics and stack preservation.
223223
- [x] Ensure runtime errors preserve original position and call frames.
224224

225225
### Runtime Behavior
@@ -238,7 +238,7 @@ Goal: improve language ergonomics for complex script logic and recovery behavior
238238
### v0.16.0 Definition of Done
239239

240240
- [x] No regressions in existing `if/for/range` behavior.
241-
- [ ] Structured error handling works with assertions and runtime errors.
241+
- [x] Structured error handling works with assertions and runtime errors.
242242
- [x] Coverage includes nested/edge control-flow paths.
243243

244244
---
@@ -249,29 +249,29 @@ Goal: make multi-file script projects easier to compose and maintain.
249249

250250
### Module System
251251

252-
- [ ] Add explicit export controls (beyond underscore naming).
253-
- [ ] Add import aliasing for module objects.
254-
- [ ] Define and enforce module namespace conflict behavior.
255-
- [ ] Improve cycle error diagnostics with concise chain rendering.
256-
- [ ] Add module cache invalidation policy for long-running hosts.
252+
- [x] Add explicit export controls (beyond underscore naming).
253+
- [x] Add import aliasing for module objects.
254+
- [x] Define and enforce module namespace conflict behavior.
255+
- [x] Improve cycle error diagnostics with concise chain rendering.
256+
- [x] Add module cache invalidation policy for long-running hosts.
257257

258258
### Security and Isolation
259259

260-
- [ ] Tighten module root boundary checks and path normalization.
261-
- [ ] Add test coverage for path traversal attempts.
262-
- [ ] Add explicit policy hooks for module allow/deny lists.
260+
- [x] Tighten module root boundary checks and path normalization.
261+
- [x] Add test coverage for path traversal attempts.
262+
- [x] Add explicit policy hooks for module allow/deny lists.
263263

264264
### Developer UX
265265

266-
- [ ] Add docs for module project layout best practices.
267-
- [ ] Add examples for reusable helper modules and namespaced imports.
268-
- [ ] Add migration guide for existing `require` users.
266+
- [x] Add docs for module project layout best practices.
267+
- [x] Add examples for reusable helper modules and namespaced imports.
268+
- [x] Add migration guide for existing `require` users.
269269

270270
### v0.17.0 Definition of Done
271271

272-
- [ ] Module APIs are explicit and predictable.
273-
- [ ] Error output for cycle/import failures is actionable.
274-
- [ ] Security invariants around module paths are fully tested.
272+
- [x] Module APIs are explicit and predictable.
273+
- [x] Error output for cycle/import failures is actionable.
274+
- [x] Security invariants around module paths are fully tested.
275275

276276
---
277277

docs/builtins.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ For time manipulation in VibeScript, use the `Time` object (`Time.now`, `Time.pa
5858

5959
## Module Loading
6060

61-
### `require(module_name)`
61+
### `require(module_name, as: alias?)`
6262

63-
Loads a module from configured module search paths and returns an object containing the module's exported functions:
63+
Loads a module from configured module search paths and returns an object containing the module's exported functions. Modules can mark exports explicitly with `export def ...`; when no explicit exports are declared, public (non-underscore) functions are exported by default. Exported functions are injected into globals only when the name is still free (existing globals keep precedence), and `as:` can be used to bind the module object explicitly:
6464

6565
```vibe
6666
def calculate_total(amount)
67-
helpers = require("fee_calculator")
67+
require("fee_calculator", as: "helpers")
6868
amount + helpers.calculate_fee(amount)
6969
end
7070
```

docs/errors.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,44 @@ Loop control diagnostics are explicit:
6262

6363
These boundary errors happen when `break`/`next` are raised inside called blocks/functions and attempt to escape into an outer loop.
6464

65+
## Structured Error Handling
66+
67+
Use `begin` with `rescue` and/or `ensure` for script-level recovery:
68+
69+
```vibe
70+
def safe_div(a, b)
71+
begin
72+
a / b
73+
rescue(RuntimeError)
74+
"fallback"
75+
ensure
76+
audit("safe_div attempted")
77+
end
78+
end
79+
```
80+
81+
Re-raise the current rescued error with `raise`:
82+
83+
```vibe
84+
begin
85+
risky_call()
86+
rescue(AssertionError)
87+
audit("recovering assertion")
88+
raise
89+
end
90+
```
91+
92+
Semantics:
93+
94+
- `rescue` runs only when the `begin` body raises an error.
95+
- `rescue` supports optional typed matching via `rescue(<Type>)`.
96+
- `rescue` supports `AssertionError`, `RuntimeError`, and unions such as `rescue(AssertionError | RuntimeError)`.
97+
- `ensure` always runs (success, rescue path, or failure path).
98+
- Without `rescue`, original runtime errors still propagate after `ensure` executes.
99+
- Unmatched typed rescues do not swallow the original error.
100+
- `raise` inside `rescue` re-raises the original error and preserves its stack frames.
101+
- `raise "message"` raises a new runtime error. Bare `raise` outside `rescue` is a runtime error.
102+
65103
## REPL Debugging
66104

67105
The REPL stores the previous failure. Use:

docs/examples/module_require.md

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ Place `modules/fees.vibe` on disk:
1818
```vibe
1919
# modules/fees.vibe
2020
21-
def rate()
22-
1
21+
export def apply_fee(amount)
22+
amount + rate()
2323
end
2424
25-
def apply_fee(amount)
26-
amount + rate()
25+
def rate()
26+
1
2727
end
2828
2929
def _rounding_hint()
@@ -37,15 +37,28 @@ A main script can load the helpers and use the exported functions:
3737
# scripts/checkout.vibe
3838
3939
def total_with_fee(amount)
40-
fees = require("fees")
40+
require("fees", as: "fees")
4141
fees.apply_fee(amount)
4242
end
4343
```
4444

45+
Namespaced imports scale better as helper sets grow:
46+
47+
```vibe
48+
def quote_total(amount)
49+
require("billing/fees", as: "fees")
50+
require("billing/taxes", as: "taxes")
51+
taxes.apply(fees.apply(amount))
52+
end
53+
```
54+
4555
When `total_with_fee` runs, `require("fees")` resolves the module relative to
4656
`Config.ModulePaths`, compiles it once, and returns an object containing the
47-
module’s public exports. Function names starting with `_` stay private to the
48-
module and are not exposed on the returned object or injected globally.
57+
module’s exports. Use `export def` for explicit control; if no explicit exports
58+
are declared, public functions are exported by default and names starting with
59+
`_` stay private to the module. When an exported name conflicts with an
60+
existing global, the existing binding keeps precedence and the module object
61+
remains the conflict-free access path.
4962

5063
Inside modules, explicit relative requires are supported:
5164

@@ -57,9 +70,24 @@ def compute(amount)
5770
end
5871
```
5972

73+
Reusable helper modules can be shared from a central namespace:
74+
75+
```vibe
76+
# modules/shared/currency.vibe
77+
export def cents(value)
78+
value * 100
79+
end
80+
81+
# modules/billing/taxes.vibe
82+
export def apply(amount)
83+
require("../shared/currency", as: "currency")
84+
amount + currency.cents(1)
85+
end
86+
```
87+
6088
Relative requires (`./` and `../`) resolve from the requiring module’s
6189
directory and cannot escape the configured module root.
6290

6391
After the first load the module is cached, making subsequent `require` calls
6492
cheap. To refresh or hot reload modules, restart the embedding application or
65-
clear the engine’s module cache.
93+
call `engine.ClearModuleCache()` between runs.

docs/integration.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,31 @@ if err != nil {
6565
}
6666

6767
script, err := engine.Compile(`def total(amount)
68-
helpers = require("fees")
68+
require("fees", as: "helpers")
6969
helpers.apply_fee(amount)
7070
end`)
7171
```
7272

7373
The interpreter searches each configured directory for `<module>.vibe` in order
7474
and caches compiled modules so subsequent calls to `require` are inexpensive.
75+
For long-running hosts, call `engine.ClearModuleCache()` between runs when
76+
module sources can change.
77+
Use `Config.ModuleAllowList` / `Config.ModuleDenyList` for policy hooks over
78+
which modules may be loaded (`*` glob patterns against normalized module names,
79+
with deny-list rules taking precedence).
80+
When a circular module dependency is detected, the runtime reports a concise
81+
chain (for example `a -> b -> a`).
82+
Use the optional `as:` keyword to bind the loaded module object to a global
83+
alias.
7584
Inside a module, use explicit relative paths (`./` or `../`) to load siblings
7685
or parent-local helpers. Relative requires are resolved from the calling
7786
module's directory and are rejected if they escape the module root. Functions
78-
whose names start with `_` are private and are not exported.
87+
can be exported explicitly with `export def ...`; if no explicit exports are
88+
declared, public names are exported by default and names starting with `_`
89+
remain private. Exported names are only injected into globals when no binding
90+
already exists, so existing host/script globals keep precedence.
91+
Import paths are normalized across slash styles, and traversal/symlink escapes
92+
outside configured module roots are blocked.
7993

8094
### Capability Adapters
8195

docs/introduction.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,9 @@ dives on specific topics.
2424
- `blocks.md` – using block literals for map/select/reduce style patterns.
2525
- `integration.md` – host integration patterns showing how Go services can
2626
expose capabilities to scripts.
27+
- `module_project_layout.md` – recommended structure for multi-module script
28+
repositories.
29+
- `module_require_migration.md` – migration checklist for modern `require`
30+
behavior (exports, aliasing, policy hooks).
2731
- `examples/module_require.md` – practical example showing how to share
2832
helpers with `require` and module search paths.

docs/module_project_layout.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Module Project Layout Best Practices
2+
3+
Use a stable directory layout so module paths stay predictable:
4+
5+
```text
6+
workflows/
7+
modules/
8+
shared/
9+
math.vibe
10+
money.vibe
11+
billing/
12+
fees.vibe
13+
taxes.vibe
14+
scripts/
15+
checkout.vibe
16+
payouts.vibe
17+
```
18+
19+
Guidelines:
20+
21+
- Keep reusable helpers under `modules/shared/`.
22+
- Group domain logic by folder (`billing/`, `risk/`, `reporting/`).
23+
- Prefer `export def ...` for public API surface, keep internals unexported.
24+
- Use `require("module/path", as: "alias")` to avoid global name collisions.
25+
- Use relative requires only within a module subtree (`./`, `../`).
26+
- Configure `ModuleAllowList` and `ModuleDenyList` in hosts that need strict import policy boundaries.
27+
28+
Example:
29+
30+
```vibe
31+
# modules/billing/fees.vibe
32+
export def apply(amount)
33+
amount + shared_rate()
34+
end
35+
36+
def shared_rate()
37+
rates = require("../shared/math", as: "math")
38+
math.double(1)
39+
end
40+
```
41+
42+
```vibe
43+
# scripts/checkout.vibe
44+
def total(amount)
45+
require("billing/fees", as: "fees")
46+
fees.apply(amount)
47+
end
48+
```

docs/module_require_migration.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Migration Guide for `require`
2+
3+
This guide helps older scripts move to the current module model.
4+
5+
## 1. Prefer explicit exports
6+
7+
Before:
8+
9+
```vibe
10+
def apply_fee(amount)
11+
amount + 1
12+
end
13+
```
14+
15+
After:
16+
17+
```vibe
18+
export def apply_fee(amount)
19+
amount + 1
20+
end
21+
```
22+
23+
If a module has no `export def`, non-underscore functions are still exported.
24+
25+
## 2. Use aliases for namespacing
26+
27+
Before:
28+
29+
```vibe
30+
fees = require("fees")
31+
fees.apply_fee(amount)
32+
```
33+
34+
After:
35+
36+
```vibe
37+
require("fees", as: "fees")
38+
fees.apply_fee(amount)
39+
```
40+
41+
Aliases make import intent explicit and reduce global collisions.
42+
43+
## 3. Plan for conflict behavior
44+
45+
- Existing globals are not overwritten by module exports.
46+
- Access conflicting functions through the returned/aliased module object.
47+
- Alias collisions raise runtime errors (`alias "<name>" already defined`).
48+
49+
## 4. Add policy boundaries in hosts
50+
51+
For long-running or multi-tenant hosts:
52+
53+
- Configure `ModuleAllowList` and `ModuleDenyList`.
54+
- Use `engine.ClearModuleCache()` when module sources may change.
55+
56+
## 5. Validate with tests
57+
58+
- Add integration tests for required module paths.
59+
- Add negative tests for denied modules and traversal attempts.
60+
- Verify cycle errors are actionable (`a -> b -> a`).

vibes/ast.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type FunctionStmt struct {
3131
ReturnTy *TypeExpr
3232
Body []Statement
3333
IsClassMethod bool
34+
Exported bool
3435
Private bool
3536
position Position
3637
}
@@ -84,6 +85,14 @@ type ReturnStmt struct {
8485
func (s *ReturnStmt) stmtNode() {}
8586
func (s *ReturnStmt) Pos() Position { return s.position }
8687

88+
type RaiseStmt struct {
89+
Value Expression
90+
position Position
91+
}
92+
93+
func (s *RaiseStmt) stmtNode() {}
94+
func (s *RaiseStmt) Pos() Position { return s.position }
95+
8796
type AssignStmt struct {
8897
Target Expression
8998
Value Expression
@@ -154,6 +163,17 @@ type NextStmt struct {
154163
func (s *NextStmt) stmtNode() {}
155164
func (s *NextStmt) Pos() Position { return s.position }
156165

166+
type TryStmt struct {
167+
Body []Statement
168+
RescueTy *TypeExpr
169+
Rescue []Statement
170+
Ensure []Statement
171+
position Position
172+
}
173+
174+
func (s *TryStmt) stmtNode() {}
175+
func (s *TryStmt) Pos() Position { return s.position }
176+
157177
type Identifier struct {
158178
Name string
159179
position Position

0 commit comments

Comments
 (0)