Fixed some bugs discovered by Claude Code
This commit is contained in:
parent
852ea7c0f7
commit
33cf23b221
57
CLAUDE.md
Normal file
57
CLAUDE.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
UCL is an embeddable command-based scripting language implemented in Go, with Tcl/shell-like syntax. It is a **library** — there is no root `main.go`. All executables live under `cmd/`.
|
||||||
|
|
||||||
|
## Build & Test Commands
|
||||||
|
|
||||||
|
- **Run all tests:** `make test` (or `go test ./ucl/...`)
|
||||||
|
- **Run a single test:** `go test ./ucl/... -run TestName`
|
||||||
|
- **Run builtin module tests:** `go test ./ucl/builtins/...`
|
||||||
|
- **Build site + WASM playground:** `make site`
|
||||||
|
- **Clean build artifacts:** `make clean`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Package (`ucl/`)
|
||||||
|
|
||||||
|
The language engine follows a **parse → tree-walk evaluate** pipeline:
|
||||||
|
|
||||||
|
- **`ast.go`** — AST node types and parser (using `participle` library with struct-tag grammar)
|
||||||
|
- **`eval.go`** — Tree-walk evaluator: scripts, statements, commands, pipelines, arguments, dot-access, string interpolation
|
||||||
|
- **`objs.go`** — Object type system: `StringObject`, `IntObject`, `BoolObject`, `ListObject`, `HashObject`, `TimeObject`, plus proxy types for Go interop via reflection
|
||||||
|
- **`inst.go`** — `Inst` (interpreter instance): the public API for creating and configuring the evaluator
|
||||||
|
- **`builtins.go`** — Built-in functions and macros (control flow, math, collections)
|
||||||
|
- **`env.go`** — `evalCtx`: scoped evaluation context (linked-list chain for variable/command lookup)
|
||||||
|
- **`userbuiltin.go`** — Public embedding API (`BuiltinHandler`, `CallArgs` with reflection-based argument binding)
|
||||||
|
|
||||||
|
### Two Command Dispatch Kinds
|
||||||
|
|
||||||
|
- **Invokables** (`invokable` interface): receive fully-evaluated arguments. Used by all regular functions, Go builtins, user `proc`s, and blocks.
|
||||||
|
- **Macros** (`macroable` interface): receive unevaluated AST + evaluator. Used by `if`, `for`, `while`, `proc`, `try` for lazy evaluation control.
|
||||||
|
|
||||||
|
### Optional Builtin Modules (`ucl/builtins/`)
|
||||||
|
|
||||||
|
Modules (`strs`, `lists`, `itrs`, `fns`, `os`, `fs`, `log`, `csv`, `time`, `urls`) are opt-in via `ucl.WithModule(builtins.Strs())` etc. Each is a self-contained file.
|
||||||
|
|
||||||
|
### Other Packages
|
||||||
|
|
||||||
|
- **`repl/`** — REPL wrapper with `EvalAndDisplay()`, help system, and `AddTypePrinter[T]` generic
|
||||||
|
- **`cmd/cmsh/`** — Interactive shell using `readline`
|
||||||
|
- **`cmd/playwasm/`** — WASM playground (builds with `GOOS=js GOARCH=wasm`)
|
||||||
|
- **`cmd/gendocs/`** — Markdown-to-HTML doc site generator (goldmark + frontmatter)
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
- Tests use `testify/assert` with **table-driven** patterns (`[]struct{ desc, expr string; want any }`)
|
||||||
|
- Test packages use the `_test` suffix for black-box testing
|
||||||
|
- Shared test helpers (e.g., `WithTestBuiltin()`) are in `ucl/builtins_test.go` and provide test-only commands (`firstarg`, `toUpper`, `sjoin`, etc.)
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Forgejo workflows in `.forgejo/workflows/`:
|
||||||
|
- `test.yaml` — runs `make test` on `feature/*` branches
|
||||||
|
- `build.yaml` — runs tests + `make site-deploy` on `main`
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ucl.lmika.dev/ucl"
|
"ucl.lmika.dev/ucl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ func (r *REPL) echoPrinter(ctx context.Context, w io.Writer, args []any) (err er
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = fmt.Fprintln(w, res)
|
_, err = fmt.Fprintln(w, res)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise bool) (err error) {
|
func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise bool) (err error) {
|
||||||
|
|
@ -56,7 +57,7 @@ func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise
|
||||||
|
|
||||||
switch v := res.(type) {
|
switch v := res.(type) {
|
||||||
case nil:
|
case nil:
|
||||||
if _, err = fmt.Fprintln(os.Stdout, "(nil)"); err != nil {
|
if _, err = fmt.Fprintln(w, "(nil)"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case ucl.Listable:
|
case ucl.Listable:
|
||||||
|
|
@ -92,6 +93,10 @@ func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "]")
|
fmt.Fprintf(w, "]")
|
||||||
} else {
|
} else {
|
||||||
|
if v == nil {
|
||||||
|
fmt.Fprintf(w, "(nil)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
// In the off-chance that this is actually a slice of printables
|
// In the off-chance that this is actually a slice of printables
|
||||||
vt := reflect.SliceOf(reflect.TypeOf(v[0]))
|
vt := reflect.SliceOf(reflect.TypeOf(v[0]))
|
||||||
if tp, ok := r.typePrinters[vt]; ok {
|
if tp, ok := r.typePrinters[vt]; ok {
|
||||||
|
|
|
||||||
|
|
@ -174,5 +174,5 @@ var parser = participle.MustBuild[astScript](participle.Lexer(scanner),
|
||||||
participle.Elide("Whitespace", "Comment"))
|
participle.Elide("Whitespace", "Comment"))
|
||||||
|
|
||||||
func parse(fname string, r io.Reader) (*astScript, error) {
|
func parse(fname string, r io.Reader) (*astScript, error) {
|
||||||
return parser.Parse("test", r)
|
return parser.Parse(fname, r)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,6 @@ func callBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
|
||||||
for i := 0; i < argList.Len(); i++ {
|
for i := 0; i < argList.Len(); i++ {
|
||||||
calledArgs[i] = argList.Index(i)
|
calledArgs[i] = argList.Index(i)
|
||||||
}
|
}
|
||||||
args.shift(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
invArgs := args.fork(calledArgs)
|
invArgs := args.fork(calledArgs)
|
||||||
|
|
@ -932,7 +931,7 @@ func (s seqObject) Len() int {
|
||||||
|
|
||||||
func (s seqObject) Index(i int) Object {
|
func (s seqObject) Index(i int) Object {
|
||||||
l := s.Len()
|
l := s.Len()
|
||||||
if i < 0 || i > l {
|
if i < 0 || i >= l {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.from > s.to {
|
if s.from > s.to {
|
||||||
|
|
@ -1028,6 +1027,9 @@ func tryBuiltin(ctx context.Context, args macroArgs) (res Object, err error) {
|
||||||
finallyBlocks = append(finallyBlocks, i+1)
|
finallyBlocks = append(finallyBlocks, i+1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(finallyBlocks) > 1 {
|
||||||
|
return nil, tooManyFinallyBlocksError(args.ast.Pos)
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if isBreakErr(err) {
|
if isBreakErr(err) {
|
||||||
|
|
@ -1122,7 +1124,13 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (Object, error) {
|
||||||
case Hashable:
|
case Hashable:
|
||||||
err := t.Each(func(k string, v Object) error {
|
err := t.Each(func(k string, v Object) error {
|
||||||
last, err = args.evalBlock(ctx, blockIdx, []Object{StringObject(k), v}, false)
|
last, err = args.evalBlock(ctx, blockIdx, []Object{StringObject(k), v}, false)
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, &breakErr) && breakErr.isCont {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
if errors.As(err, &breakErr) {
|
if errors.As(err, &breakErr) {
|
||||||
if !breakErr.isCont {
|
if !breakErr.isCont {
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ func eachListOrIterItem(ctx context.Context, o ucl.Object, f func(int, ucl.Objec
|
||||||
}
|
}
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return errors.New("expected listable")
|
return errors.New("expected listable")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"ucl.lmika.dev/ucl"
|
"ucl.lmika.dev/ucl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -36,9 +37,10 @@ func (lh logHandler) logMessage(ctx context.Context, args ucl.CallArgs) (any, er
|
||||||
var s ucl.Object
|
var s ucl.Object
|
||||||
|
|
||||||
for args.NArgs() > 0 {
|
for args.NArgs() > 0 {
|
||||||
if err := args.Bind(&s); err == nil {
|
if err := args.Bind(&s); err != nil {
|
||||||
sb.WriteString(s.String())
|
return nil, err
|
||||||
}
|
}
|
||||||
|
sb.WriteString(s.String())
|
||||||
}
|
}
|
||||||
return nil, lh.handler(sb.String())
|
return nil, lh.handler(sb.String())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ func (ec *evalCtx) lookupInvokable(name string) invokable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ec.parent.lookupInvokable(name)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *evalCtx) lookupMacro(name string) macroable {
|
func (ec *evalCtx) lookupMacro(name string) macroable {
|
||||||
|
|
@ -127,5 +127,5 @@ func (ec *evalCtx) lookupMacro(name string) macroable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ec.parent.lookupMacro(name)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,7 @@ func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVa
|
||||||
}
|
}
|
||||||
return toVal, nil
|
return toVal, nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("unknown pseudo-variable: " + *n.Var)
|
return nil, fmt.Errorf("unknown pseudo-variable: %v", n.PseudoVar)
|
||||||
case n.MaybeSub != nil:
|
case n.MaybeSub != nil:
|
||||||
return nil, errors.New("cannot assign to a subexpression")
|
return nil, errors.New("cannot assign to a subexpression")
|
||||||
case n.ListOrHash != nil:
|
case n.ListOrHash != nil:
|
||||||
|
|
|
||||||
11
ucl/objs.go
11
ucl/objs.go
|
|
@ -444,17 +444,6 @@ func (ia invocationArgs) expectArgn(x int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ia invocationArgs) stringArg(i int) (string, error) {
|
|
||||||
if len(ia.args) < i {
|
|
||||||
return "", errors.New("expected at least " + strconv.Itoa(i) + " args")
|
|
||||||
}
|
|
||||||
s, ok := ia.args[i].(fmt.Stringer)
|
|
||||||
if !ok {
|
|
||||||
return "", errors.New("expected a string arg")
|
|
||||||
}
|
|
||||||
return s.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ia invocationArgs) intArg(i int) (int, error) {
|
func (ia invocationArgs) intArg(i int) (int, error) {
|
||||||
if len(ia.args) < i {
|
if len(ia.args) < i {
|
||||||
return 0, errors.New("expected at least " + strconv.Itoa(i) + " args")
|
return 0, errors.New("expected at least " + strconv.Itoa(i) + " args")
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,6 @@ func canBindProxyObject(v interface{}, r reflect.Value) bool {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if r.Type().AssignableTo(argValue.Elem().Type()) {
|
if r.Type().AssignableTo(argValue.Elem().Type()) {
|
||||||
argValue.Elem().Set(r)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if r.Type().Kind() != reflect.Pointer {
|
if r.Type().Kind() != reflect.Pointer {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue