diff --git a/Makefile b/Makefile index 626870d..0bb4177 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,10 @@ test: site: clean mkdir build mkdir build/site + mkdir build/site/core cp -r _site/* build/site/. + go run ./cmd/gendocs/main.go ./_docs/index.md > build/site/index.html + go run ./cmd/gendocs/main.go ./_docs/core.md > build/site/core/index.html GOOS=js GOARCH=wasm go build -o build/site/playwasm.wasm ./cmd/playwasm/. site-deploy: site diff --git a/_docs/core.md b/_docs/core.md new file mode 100644 index 0000000..27b4ed4 --- /dev/null +++ b/_docs/core.md @@ -0,0 +1,119 @@ +--- +--- + +# Core Functions + +### call + +``` +call BLOCK [ARGS...] +``` + +Invokes block, passing in the arguments. This is as if block was invoked as a command. This may not always be necessary +unless these are need to call BLOCK with exactly 0 arguments. + +### echo + +``` +echo [ARGS...] +``` + +Displays the string representation of ARGS to stdout followed by a new line. + +### foreach + +``` +foreach SEQ BLOCK +``` + +Iterates BLOCK over every element of the sequence. + +The values pass to BLOCK will depend on the type of SEQ. If SEQ is a list, BLOCK receives the element value. +If SEQ is a hash, BLOCK receives both the key and value of each element. + +BLOCK can call `break` and `continue` which will exit out of the loop, or jump to the start of the next iteration +respectively. + +The return value of `foreach` will be the result of the last iteration, unless `break` is called with a value. + +``` +foreach [1 2 3] { |e| + echo "Element = $e" +} + +foreach [a:"one" b:"two"] { |k v| + echo "Key $k = $v" +} +``` + +### keys + +``` +keys HASH +``` + +Returns the keys of the passed in hash as a list. The order of keys are non-deterministic. +If HASH is not a hash, then nil will be returned. + +### len + +``` +len SEQ +``` + +Returns the length of SEQ. If SEQ is a list or hash, SEQ will be the number of +elements. If SEQ is a string, SEQ will be the string's length. All other values will +return a length of 0. + +### map + +``` +map SEQ BLOCK +``` + +Returns a new list of elements mapped from SEQ according to the result of BLOCK. SEQ can be any listable data +structure, however the result will always be a concrete list. + +``` +map [1 2 3] { |x| str $x | len } +``` + +### proc + +``` +proc [NAME] BLOCK +``` + +Defines a new function optionally with the given name. When called without NAME, this will define a new +lambda which can be invoked using `call`. + +When NAME is set, this function defining a function a name will always declare it at the top-level scope. + +### reduce + +``` +reduce SEQ [INIT] BLOCK +``` + +Returns the result of reducing the elements of SEQ with the passed in block. + +BLOCK will receive at least two argument, with the current value of the accumulator always being the last argument. +If SEQ is a list, the arguments will be _|element accumulator|_, and if SEQ is a hash, the arguments will be +_|key value accumulator|_. + +The block result will be set as the value of the accumulator for the next iteration. Once all elements are process +the accumulator value will be returned as the result of `reduce`. + +If INIT is not set, and SEQ is a list, the accumulator will be set to the first value and BLOCK will be called +from the second element, if any. If SEQ is a hash, then the accumulator will be set to nil. + +### set + +``` +set NAME VALUE +``` + +Sets the value of variable NAME to VALUE. Any variable with NAME will be checked +within the scope first, including any parent scopes, before a new variable is defined. +Any new variables will only be defined with the current scope. + diff --git a/_docs/index.md b/_docs/index.md new file mode 100644 index 0000000..f535869 --- /dev/null +++ b/_docs/index.md @@ -0,0 +1,22 @@ +# Universal Command Language + +Universal Command Language, or UCL, is a scripting language designed for use with REPLs or batch jobs. +It's heavily inspired by the likes of TCL and Bash, and it's purpose is to be usable as an interactive command +language while at the same time being useful enough as a scripting language for simple automation tasks. + +The flavour of a particular UCL instance will depend heavily on the host environment. Most likely additional commands +will be defined, and some commands may be removed. But this page describes the features of a "typical" UCL instance. + +## Example + +``` +proc greet { |someone| + echo "Hello, $someone" +} + +greet "world" +``` + +## Status + +This is very much still in development and is subject to change. \ No newline at end of file diff --git a/_site/index.html b/_site/index.html index 15ea466..6508f95 100644 --- a/_site/index.html +++ b/_site/index.html @@ -12,23 +12,10 @@
-

Playground

- -
- - -
\ No newline at end of file diff --git a/_site/playground/index.html b/_site/playground/index.html new file mode 100644 index 0000000..fcba6ea --- /dev/null +++ b/_site/playground/index.html @@ -0,0 +1,39 @@ + + + + + + + + + +
+

UCL

+ +
+ +
+

Playground

+ +
+ + + +
+ + + + \ No newline at end of file diff --git a/_site/style.css b/_site/style.css index 34bb4fa..6622960 100644 --- a/_site/style.css +++ b/_site/style.css @@ -2,4 +2,4 @@ border: solid 4px black; border-radius: 5px; scrollbar-color: white black; -} \ No newline at end of file +} diff --git a/cmd/gendocs/frame.tmpl b/cmd/gendocs/frame.tmpl new file mode 100644 index 0000000..2c5e142 --- /dev/null +++ b/cmd/gendocs/frame.tmpl @@ -0,0 +1,25 @@ + + + + + + + +
+

UCL

+ +
+ +
+ {{.Body}} +
+ + + + \ No newline at end of file diff --git a/cmd/gendocs/main.go b/cmd/gendocs/main.go new file mode 100644 index 0000000..e53b1e7 --- /dev/null +++ b/cmd/gendocs/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "bytes" + "embed" + "flag" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/parser" + "go.abhg.dev/goldmark/frontmatter" + "html/template" + "log" + "os" +) + +//go:embed frame.tmpl +var frameTmpl embed.FS + +func main() { + flag.Parse() + + if flag.NArg() != 1 { + log.Fatalln("usage: gendocs [markdown]") + } + + md := goldmark.New(goldmark.WithExtensions(&frontmatter.Extender{})) + + mdData, err := os.ReadFile(flag.Arg(0)) + if err != nil { + log.Fatal(err) + } + + ctx := parser.NewContext() + + var buf bytes.Buffer + if err := md.Convert(mdData, &buf, parser.WithContext(ctx)); err != nil { + log.Fatal(err) + } + + var frontMatter struct { + Title string `yaml:"title"` + } + if fm := frontmatter.Get(ctx); fm != nil { + if err := fm.Decode(&frontMatter); err != nil { + log.Fatal(err) + } + } + + frameTmpls, err := template.ParseFS(frameTmpl, "*.tmpl") + if err != nil { + log.Fatal(err) + } + var res bytes.Buffer + if err := frameTmpls.ExecuteTemplate(&res, "frame.tmpl", map[string]interface{}{ + "Title": frontMatter.Title, + "Body": template.HTML(buf.Bytes()), + }); err != nil { + log.Fatal(err) + } + + os.Stdout.Write(res.Bytes()) +} diff --git a/go.mod b/go.mod index ab85d93..981a55a 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,15 @@ module ucl.lmika.dev go 1.21.1 require ( - github.com/alecthomas/participle/v2 v2.1.1 // indirect - github.com/chzyer/readline v1.5.1 // indirect + github.com/alecthomas/participle/v2 v2.1.1 + github.com/chzyer/readline v1.5.1 + github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/BurntSushi/toml v1.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.9.0 // indirect golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect diff --git a/go.sum b/go.sum index 21d3bb6..f9cdee7 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,35 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f h1:tz68Lhc1oR15HVz69IGbtdukdH0x70kBDEvvj5pTXyE= github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f/go.mod h1:zHQvhjGXRro/Xp2C9dbC+ZUpE0gL4GYW75x1lk7hwzI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= +go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/repl/evaldisplay.go b/repl/evaldisplay.go index d062778..a37e74f 100644 --- a/repl/evaldisplay.go +++ b/repl/evaldisplay.go @@ -2,6 +2,7 @@ package repl import ( "context" + "errors" "fmt" "io" "os" @@ -15,6 +16,9 @@ type NoResults struct{} func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error { res, err := r.inst.Eval(ctx, expr) if err != nil { + if errors.Is(err, ucl.ErrNotConvertable) { + return nil + } return err } diff --git a/ucl/builtins.go b/ucl/builtins.go index fd405e6..82cc2f9 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -196,17 +196,6 @@ func setBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return newVal, nil } -func toUpperBuiltin(ctx context.Context, args invocationArgs) (Object, error) { - if err := args.expectArgn(1); err != nil { - return nil, err - } - sarg, err := args.stringArg(0) - if err != nil { - return nil, err - } - return StringObject(strings.ToUpper(sarg)), nil -} - func eqBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err diff --git a/ucl/inst.go b/ucl/inst.go index 1732819..697273d 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -57,7 +57,6 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("echo", invokableFunc(echoBuiltin)) rootEC.addCmd("set", invokableFunc(setBuiltin)) - rootEC.addCmd("toUpper", invokableFunc(toUpperBuiltin)) rootEC.addCmd("len", invokableFunc(lenBuiltin)) rootEC.addCmd("index", invokableFunc(indexBuiltin)) rootEC.addCmd("call", invokableFunc(callBuiltin)) diff --git a/ucl/userbuiltin_test.go b/ucl/userbuiltin_test.go index 575f238..adb657a 100644 --- a/ucl/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -299,6 +299,74 @@ func TestCallArgs_CanBind(t *testing.T) { assert.Equal(t, tt.want, res) }) } + + t.Run("can bind invokable", func(t *testing.T) { + inst := ucl.New() + inst.SetBuiltin("toUpper", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var s string + if err := args.Bind(&s); err != nil { + return nil, err + } + return strings.ToUpper(s), nil + }) + inst.SetBuiltin("wrap", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var inv ucl.Invokable + + if err := args.Bind(&inv); err != nil { + return nil, err + } + + res, err := inv.Invoke(ctx, "hello") + if err != nil { + return nil, err + } + + return fmt.Sprintf("[[%v]]", res), nil + }) + + ctx := context.Background() + + res, err := inst.Eval(ctx, `wrap { |x| toUpper $x }`) + assert.NoError(t, err) + assert.Equal(t, "[[HELLO]]", res) + }) + + t.Run("can carry invokable outside of context", func(t *testing.T) { + inst := ucl.New() + var inv ucl.Invokable + inst.SetBuiltin("toUpper", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var s string + if err := args.Bind(&s); err != nil { + return nil, err + } + return strings.ToUpper(s), nil + }) + inst.SetBuiltin("wrap", func(ctx context.Context, args ucl.CallArgs) (any, error) { + if err := args.Bind(&inv); err != nil { + return nil, err + } + + return nil, nil + }) + + ctx := context.Background() + + assert.True(t, inv.IsNil()) + + before, err := inv.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Nil(t, before) + + res, err := inst.Eval(ctx, `wrap { |x| toUpper $x }`) + assert.NoError(t, err) + assert.Nil(t, res) + + assert.False(t, inv.IsNil()) + + after, err := inv.Invoke(ctx, "hello") + assert.NoError(t, err) + assert.Equal(t, "HELLO", after) + }) } func TestCallArgs_MissingCommandHandler(t *testing.T) {