Skip to content

Commit 3a6e9f2

Browse files
authored
Merge pull request #50 from mgomes/mgomes/v0-19-0
v0.19.0: tooling quality and performance
2 parents 2fcdfd7 + dc479cc commit 3a6e9f2

36 files changed

+2065
-68
lines changed

.github/workflows/benchmarks.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: benchmarks
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
benchmark:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Go
17+
uses: actions/setup-go@v5
18+
with:
19+
go-version-file: go.mod
20+
21+
- name: Run benchmarks
22+
run: |
23+
mkdir -p .bench
24+
go test ./vibes -run '^$' -bench '^BenchmarkExecution' -benchmem | tee .bench/benchmark.txt
25+
26+
- name: Publish benchmark summary
27+
run: |
28+
{
29+
echo "## Benchmark Results"
30+
echo ""
31+
echo '```text'
32+
cat .bench/benchmark.txt
33+
echo '```'
34+
} >> "$GITHUB_STEP_SUMMARY"
35+
36+
- name: Upload benchmark artifact
37+
uses: actions/upload-artifact@v4
38+
with:
39+
name: benchmark-results
40+
path: .bench/benchmark.txt
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: release-checklist
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: "Release version (vMAJOR.MINOR.PATCH)"
11+
required: true
12+
type: string
13+
14+
jobs:
15+
checklist:
16+
runs-on: ubuntu-latest
17+
env:
18+
RELEASE_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Validate release checklist
24+
run: ./scripts/release_checklist.sh "${RELEASE_VERSION}"

.github/workflows/test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ jobs:
3737
if: runner.os != 'Windows'
3838
run: just lint
3939

40+
- name: Check Vibe formatting
41+
if: runner.os != 'Windows'
42+
run: go run ./cmd/vibes fmt -check .
43+
4044
- name: Run tests (Windows)
4145
if: runner.os == 'Windows'
4246
shell: pwsh
@@ -52,7 +56,32 @@ jobs:
5256
exit 1
5357
}
5458
59+
- name: Check Vibe formatting (Windows)
60+
if: runner.os == 'Windows'
61+
shell: pwsh
62+
run: go run ./cmd/vibes fmt -check .
63+
5564
- name: Run golangci-lint (Windows)
5665
if: runner.os == 'Windows'
5766
shell: pwsh
5867
run: golangci-lint run --timeout=10m --out-format=colored-line-number
68+
69+
quality-gates:
70+
runs-on: ubuntu-latest
71+
steps:
72+
- name: Checkout
73+
uses: actions/checkout@v4
74+
75+
- name: Set up Go
76+
uses: actions/setup-go@v5
77+
with:
78+
go-version-file: go.mod
79+
80+
- name: Gate examples coverage
81+
run: go test ./vibes -run 'TestExamples|TestDocsExampleSnippetsCompile'
82+
83+
- name: Gate parser fuzz target
84+
run: go test ./vibes -run '^$' -fuzz=FuzzCompileScriptDoesNotPanic -fuzztime=5s
85+
86+
- name: Gate runtime fuzz target
87+
run: go test ./vibes -run '^$' -fuzz=FuzzRuntimeEdgeCasesDoNotPanic -fuzztime=5s

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## Unreleased
6+
7+
- Ongoing work toward `v0.19.0` tooling and quality milestones.
8+
9+
## v0.19.0 - Unreleased
10+
11+
- Milestone in progress. Use this section for release-ready change summaries.
12+
13+
## v0.1.0 - 2026-02-19
14+
15+
- Initial pre-1.0 project baseline and public documentation set.

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ The REPL maintains a persistent environment, so variables assigned in one expres
111111
| -------- | ---------------------- |
112112
| `:help` | Toggle help panel |
113113
| `:vars` | Toggle variables panel |
114+
| `:globals` | Print current globals |
115+
| `:functions` | List callable functions |
116+
| `:types` | Show global value types |
114117
| `:last_error` | Show previous error |
115118
| `:clear` | Clear output history |
116119
| `:reset` | Reset the environment |
@@ -157,22 +160,31 @@ Long-form guides live in `docs/`:
157160
- `docs/errors.md` – parse/runtime error formatting and debugging patterns.
158161
- `docs/control-flow.md` – conditionals, loops, and ranges.
159162
- `docs/blocks.md` – working with block literals for enumerable-style operations.
163+
- `docs/tooling.md` – CLI workflows for running, formatting, analyzing, language-server usage, and REPL usage.
160164
- `docs/integration.md` – integrating the interpreter in Go applications.
161165
- `docs/durations.md` – duration literals, conversions, and arithmetic.
162166
- `docs/time.md` – Time creation, formatting with Go layouts, accessors, and time/duration math.
163167
- `docs/typing.md` – gradual typing: annotations, nullable `?`, positional/keyword binding, and return checks.
164168
- `docs/examples/` – runnable scenario guides (campaign reporting, rewards, notifications, module usage, and more).
165169
- `docs/releasing.md` – GoReleaser workflow for changelog and GitHub release automation.
170+
- `docs/compatibility.md` – supported Go versions and CI coverage notes.
166171
- `ROADMAP.md` – versioned implementation checklist and release roadmap.
167172

168173
## Development
169174

170175
This repository uses [Just](https://github.com/casey/just) for common tasks:
171176

172177
- `just test` runs the full Go test suite (`go test ./...`).
178+
- `just bench` runs the core execution benchmarks (`go test ./vibes -run '^$' -bench '^BenchmarkExecution' -benchmem`).
173179
- `just lint` checks formatting (`gofmt`) and runs `golangci-lint` with a generous timeout.
180+
- `vibes fmt <path>` applies canonical formatting to `.vibe` files (`-check` for CI, `-w` to write).
181+
- `vibes analyze <script.vibe>` runs script-level lint checks (e.g., unreachable statements).
182+
- `vibes lsp` starts the language server protocol prototype (hover/completion/diagnostics over stdio).
174183
- Add new recipes in the `Justfile` as workflows grow.
175184

185+
CI also publishes benchmark artifacts via `.github/workflows/benchmarks.yml` on
186+
pull requests and pushes to `master`.
187+
176188
Contributions should run `just test` and `just lint` (or the equivalent `go` and `golangci-lint` commands) before submitting patches.
177189

178190
## Runtime Sandbox & Limits

ROADMAP.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -319,29 +319,29 @@ Goal: improve day-to-day developer productivity and interpreter robustness.
319319

320320
### Tooling
321321

322-
- [ ] Add canonical formatter command and CI check.
323-
- [ ] Add language server protocol (LSP) prototype (hover, completion, diagnostics).
324-
- [ ] Add static analysis command for script-level linting.
325-
- [ ] Improve REPL inspection commands (globals/functions/types).
322+
- [x] Add canonical formatter command and CI check.
323+
- [x] Add language server protocol (LSP) prototype (hover, completion, diagnostics).
324+
- [x] Add static analysis command for script-level linting.
325+
- [x] Improve REPL inspection commands (globals/functions/types).
326326

327327
### Runtime Quality
328328

329-
- [ ] Profile evaluator hotspots and optimize dispatch paths.
330-
- [ ] Reduce allocations in common value transformations.
331-
- [ ] Improve error rendering for deeply nested call stacks.
332-
- [ ] Add fuzz tests for parser and runtime edge cases.
329+
- [x] Profile evaluator hotspots and optimize dispatch paths.
330+
- [x] Reduce allocations in common value transformations.
331+
- [x] Improve error rendering for deeply nested call stacks.
332+
- [x] Add fuzz tests for parser and runtime edge cases.
333333

334334
### CI and Release Engineering
335335

336-
- [ ] Add smoke tests for docs examples to CI.
337-
- [ ] Add release checklist automation for changelog/version bumps.
338-
- [ ] Add compatibility matrix notes for supported Go versions.
336+
- [x] Add smoke tests for docs examples to CI.
337+
- [x] Add release checklist automation for changelog/version bumps.
338+
- [x] Add compatibility matrix notes for supported Go versions.
339339

340340
### v0.19.0 Definition of Done
341341

342-
- [ ] Tooling commands are documented and stable.
343-
- [ ] Performance regressions are tracked with benchmarks.
344-
- [ ] CI includes example and fuzz coverage gates.
342+
- [x] Tooling commands are documented and stable.
343+
- [x] Performance regressions are tracked with benchmarks.
344+
- [x] CI includes example and fuzz coverage gates.
345345

346346
---
347347

cmd/vibes/analyze.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"sort"
10+
11+
"github.com/mgomes/vibescript/vibes"
12+
)
13+
14+
type lintWarning struct {
15+
Function string
16+
Pos vibes.Position
17+
Message string
18+
}
19+
20+
func analyzeCommand(args []string) error {
21+
fs := flag.NewFlagSet("analyze", flag.ContinueOnError)
22+
fs.SetOutput(new(flagErrorSink))
23+
if err := fs.Parse(args); err != nil {
24+
return err
25+
}
26+
27+
remaining := fs.Args()
28+
if len(remaining) == 0 {
29+
return errors.New("vibes analyze: script path required")
30+
}
31+
32+
scriptPath, err := filepath.Abs(remaining[0])
33+
if err != nil {
34+
return fmt.Errorf("resolve script path: %w", err)
35+
}
36+
input, err := os.ReadFile(scriptPath)
37+
if err != nil {
38+
return fmt.Errorf("read script: %w", err)
39+
}
40+
41+
engine := vibes.MustNewEngine(vibes.Config{})
42+
script, err := engine.Compile(string(input))
43+
if err != nil {
44+
return fmt.Errorf("analysis compile failed: %w", err)
45+
}
46+
47+
warnings := analyzeScriptWarnings(script)
48+
if len(warnings) == 0 {
49+
fmt.Println("No issues found")
50+
return nil
51+
}
52+
53+
for _, warning := range warnings {
54+
line := warning.Pos.Line
55+
column := warning.Pos.Column
56+
if line <= 0 {
57+
line = 1
58+
}
59+
if column <= 0 {
60+
column = 1
61+
}
62+
fmt.Printf("%s:%d:%d: %s (%s)\n", scriptPath, line, column, warning.Message, warning.Function)
63+
}
64+
65+
return fmt.Errorf("analysis found %d issue(s)", len(warnings))
66+
}
67+
68+
func analyzeScriptWarnings(script *vibes.Script) []lintWarning {
69+
warnings := make([]lintWarning, 0)
70+
for _, fn := range script.Functions() {
71+
lintStatements(fn.Name, fn.Body, &warnings)
72+
}
73+
for _, classDef := range script.Classes() {
74+
for _, method := range sortedFunctionsByName(classDef.Methods) {
75+
lintStatements(classDef.Name+"#"+method.Name, method.Body, &warnings)
76+
}
77+
for _, method := range sortedFunctionsByName(classDef.ClassMethods) {
78+
lintStatements(classDef.Name+"."+method.Name, method.Body, &warnings)
79+
}
80+
}
81+
82+
sort.SliceStable(warnings, func(i, j int) bool {
83+
if warnings[i].Pos.Line != warnings[j].Pos.Line {
84+
return warnings[i].Pos.Line < warnings[j].Pos.Line
85+
}
86+
if warnings[i].Pos.Column != warnings[j].Pos.Column {
87+
return warnings[i].Pos.Column < warnings[j].Pos.Column
88+
}
89+
return warnings[i].Function < warnings[j].Function
90+
})
91+
92+
return warnings
93+
}
94+
95+
func sortedFunctionsByName(functions map[string]*vibes.ScriptFunction) []*vibes.ScriptFunction {
96+
names := make([]string, 0, len(functions))
97+
for name := range functions {
98+
names = append(names, name)
99+
}
100+
sort.Strings(names)
101+
sorted := make([]*vibes.ScriptFunction, 0, len(names))
102+
for _, name := range names {
103+
sorted = append(sorted, functions[name])
104+
}
105+
return sorted
106+
}
107+
108+
func lintStatements(function string, statements []vibes.Statement, warnings *[]lintWarning) bool {
109+
terminated := false
110+
for _, stmt := range statements {
111+
if terminated {
112+
*warnings = append(*warnings, lintWarning{
113+
Function: function,
114+
Pos: stmt.Pos(),
115+
Message: "unreachable statement",
116+
})
117+
continue
118+
}
119+
if statementTerminates(function, stmt, warnings) {
120+
terminated = true
121+
}
122+
}
123+
return terminated
124+
}
125+
126+
func statementTerminates(function string, stmt vibes.Statement, warnings *[]lintWarning) bool {
127+
switch typed := stmt.(type) {
128+
case *vibes.ReturnStmt, *vibes.RaiseStmt:
129+
return true
130+
case *vibes.IfStmt:
131+
return ifStatementTerminates(function, typed, warnings)
132+
case *vibes.ForStmt:
133+
lintStatements(function, typed.Body, warnings)
134+
return false
135+
case *vibes.WhileStmt:
136+
lintStatements(function, typed.Body, warnings)
137+
return false
138+
case *vibes.UntilStmt:
139+
lintStatements(function, typed.Body, warnings)
140+
return false
141+
case *vibes.TryStmt:
142+
bodyTerminated := lintStatements(function, typed.Body, warnings)
143+
rescueTerminated := false
144+
if len(typed.Rescue) > 0 {
145+
rescueTerminated = lintStatements(function, typed.Rescue, warnings)
146+
}
147+
ensureTerminated := false
148+
if len(typed.Ensure) > 0 {
149+
ensureTerminated = lintStatements(function, typed.Ensure, warnings)
150+
}
151+
if ensureTerminated {
152+
return true
153+
}
154+
if len(typed.Rescue) == 0 {
155+
return bodyTerminated
156+
}
157+
return bodyTerminated && rescueTerminated
158+
default:
159+
return false
160+
}
161+
}
162+
163+
func ifStatementTerminates(function string, stmt *vibes.IfStmt, warnings *[]lintWarning) bool {
164+
consequentTerminated := lintStatements(function, stmt.Consequent, warnings)
165+
elseIfAllTerminated := true
166+
for _, elseIf := range stmt.ElseIf {
167+
if !lintStatements(function, elseIf.Consequent, warnings) {
168+
elseIfAllTerminated = false
169+
}
170+
}
171+
if len(stmt.Alternate) == 0 {
172+
return false
173+
}
174+
alternateTerminated := lintStatements(function, stmt.Alternate, warnings)
175+
return consequentTerminated && elseIfAllTerminated && alternateTerminated
176+
}

0 commit comments

Comments
 (0)