From 49f2bae9e18f5f910016e1e22cf3c5c657e77edd Mon Sep 17 00:00:00 2001 From: sammyette Date: Sun, 15 Jun 2025 16:25:07 -0400 Subject: [PATCH] feat: add yarn library (#355) --- CHANGELOG.md | 2 + api.go | 6 +- cmd/docgen/docgen.go | 1 + docs/api/yarn.md | 51 +++++++++++++++ emmyLuaDocs/yarn.lua | 9 +++ exec.go | 10 +-- golibs/yarn/yarn.go | 147 +++++++++++++++++++++++++++++++++++++++++++ lua.go | 91 +++++++++++++++++---------- 8 files changed, 277 insertions(+), 40 deletions(-) create mode 100644 docs/api/yarn.md create mode 100644 emmyLuaDocs/yarn.lua create mode 100644 golibs/yarn/yarn.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e82fdd7..bc44233b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and the dot operator will cause errors in 3.0. Example: `hilbish.editor.getLine()` should be changed to `hilbish.editor:getLine()` before 3.0 - Added the `hilbish.editor:read` and `hilbish.editor:log(text)` functions. +- `yarn` threading library (See the docs) + ### Changed - Documentation for Lunacolors has been improved, with more information added. - Values returned by bait hooks will be passed to the `throw` caller diff --git a/api.go b/api.go index ccbbdfae..3d0ba262 100644 --- a/api.go +++ b/api.go @@ -62,7 +62,9 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { mod := rt.NewTable() util.SetExports(rtm, mod, exports) - hshMod = mod + if hshMod == nil { + hshMod = mod + } host, _ := os.Hostname() username := curuser.Username @@ -129,7 +131,7 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { pluginModule := moduleLoader(rtm) mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule)) - sinkModule := util.SinkLoader(l) + sinkModule := util.SinkLoader(rtm) mod.Set(rt.StringValue("sink"), rt.TableValue(sinkModule)) return rt.TableValue(mod), nil diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index 4961a648..202dffab 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -86,6 +86,7 @@ var prefix = map[string]string{ "terminal": "term", "snail": "snail", "readline": "rl", + "yarn": "yarn", } func getTagsAndDocs(docs string) (map[string][]tag, []string) { diff --git a/docs/api/yarn.md b/docs/api/yarn.md new file mode 100644 index 00000000..a9602209 --- /dev/null +++ b/docs/api/yarn.md @@ -0,0 +1,51 @@ +--- +title: Module yarn +description: multi threading library +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction +Yarn is a simple multithreading library. Threads are individual Lua states, +so they do NOT share the same environment as the code that runs the thread. +Bait and Commanders are shared though, so you *can* throw hooks from 1 thread to another. + +Example: + +```lua +local yarn = require 'yarn' + +-- calling t will run the yarn thread. +local t = yarn.thread(print) +t 'printing from another lua state!' +``` + +## Functions +||| +|----|----| +|thread(fun) -> @Thread|Creates a new, fresh Yarn thread.| + +
+
+

+yarn.thread(fun) -> Thread + + + +

+ +Creates a new, fresh Yarn thread. +`fun` is the function that will run in the thread. + +#### Parameters +This function has no parameters. +
+ +## Types +
+ +## Thread + +### Methods diff --git a/emmyLuaDocs/yarn.lua b/emmyLuaDocs/yarn.lua new file mode 100644 index 00000000..3fa4f431 --- /dev/null +++ b/emmyLuaDocs/yarn.lua @@ -0,0 +1,9 @@ +--- @meta + +local yarn = {} + +--- Creates a new, fresh Yarn thread. +--- `fun` is the function that will run in the thread. +function yarn.thread(fun) end + +return yarn diff --git a/exec.go b/exec.go index 4ed53a02..63dac576 100644 --- a/exec.go +++ b/exec.go @@ -29,12 +29,12 @@ func handleLua(input string) (string, uint8, error) { chunk, err := l.CompileAndLoadLuaChunk("", []byte(cmdString), rt.TableValue(l.GlobalEnv())) if err != nil && noexecute { fmt.Println(err) - /* if lerr, ok := err.(*lua.ApiError); ok { - if perr, ok := lerr.Cause.(*parse.Error); ok { - print(perr.Pos.Line == parse.EOF) + /* if lerr, ok := err.(*lua.ApiError); ok { + if perr, ok := lerr.Cause.(*parse.Error); ok { + print(perr.Pos.Line == parse.EOF) + } } - } - */ + */ return cmdString, 125, err } // And if there's no syntax errors and -n isnt provided, run diff --git a/golibs/yarn/yarn.go b/golibs/yarn/yarn.go new file mode 100644 index 00000000..9121e70b --- /dev/null +++ b/golibs/yarn/yarn.go @@ -0,0 +1,147 @@ +// multi threading library +// Yarn is a simple multithreading library. Threads are individual Lua states, +// so they do NOT share the same environment as the code that runs the thread. +// Bait and Commanders are shared though, so you *can* throw hooks from 1 thread to another. +/* +Example: + +```lua +local yarn = require 'yarn' + +-- calling t will run the yarn thread. +local t = yarn.thread(print) +t 'printing from another lua state!' +``` +*/ +package yarn + +import ( + "fmt" + "hilbish/util" + "os" + + "github.com/arnodel/golua/lib/packagelib" + rt "github.com/arnodel/golua/runtime" +) + +var yarnMetaKey = rt.StringValue("hshyarn") +var globalSpool *Yarn + +type Yarn struct { + initializer func(*rt.Runtime) + Loader packagelib.Loader +} + +// #type +type Thread struct { + rtm *rt.Runtime + f rt.Callable +} + +func New(init func(*rt.Runtime)) *Yarn { + yrn := &Yarn{ + initializer: init, + } + yrn.Loader = packagelib.Loader{ + Load: yrn.loaderFunc, + Name: "yarn", + } + + globalSpool = yrn + + return yrn +} + +func (y *Yarn) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + yarnMeta := rt.NewTable() + yarnMeta.Set(rt.StringValue("__call"), rt.FunctionValue(rt.NewGoFunction(yarnrun, "__call", 1, true))) + rtm.SetRegistry(yarnMetaKey, rt.TableValue(yarnMeta)) + + exports := map[string]util.LuaExport{ + "thread": { + Function: yarnthread, + ArgNum: 1, + Variadic: false, + }, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + + return rt.TableValue(mod), nil +} + +func (y *Yarn) init(th *Thread) { + y.initializer(th.rtm) +} + +// thread(fun) -> @Thread +// Creates a new, fresh Yarn thread. +// `fun` is the function that will run in the thread. +func yarnthread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + fun, err := c.CallableArg(0) + if err != nil { + return nil, err + } + + yrn := &Thread{ + rtm: rt.New(os.Stdout), + f: fun, + } + globalSpool.init(yrn) + + return c.PushingNext(t.Runtime, rt.UserDataValue(yarnUserData(t.Runtime, yrn))), nil +} + +func yarnrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + yrn, err := yarnArg(c, 0) + if err != nil { + return nil, err + } + + yrn.Run(c.Etc()) + + return c.Next(), nil +} + +func (y *Thread) Run(args []rt.Value) { + go func() { + term := rt.NewTerminationWith(y.rtm.MainThread().CurrentCont(), 0, true) + err := rt.Call(y.rtm.MainThread(), rt.FunctionValue(y.f), args, term) + if err != nil { + panic(err) + } + }() +} + +func yarnArg(c *rt.GoCont, arg int) (*Thread, error) { + j, ok := valueToYarn(c.Arg(arg)) + if !ok { + return nil, fmt.Errorf("#%d must be a yarn thread", arg+1) + } + + return j, nil +} + +func valueToYarn(val rt.Value) (*Thread, bool) { + u, ok := val.TryUserData() + if !ok { + return nil, false + } + + j, ok := u.Value().(*Thread) + return j, ok +} + +func yarnUserData(rtm *rt.Runtime, t *Thread) *rt.UserData { + yarnMeta := rtm.Registry(yarnMetaKey) + return rt.NewUserData(t, yarnMeta.AsTable()) +} diff --git a/lua.go b/lua.go index 1c407000..3a51eaaf 100644 --- a/lua.go +++ b/lua.go @@ -10,6 +10,7 @@ import ( "hilbish/golibs/fs" "hilbish/golibs/snail" "hilbish/golibs/terminal" + "hilbish/golibs/yarn" "hilbish/util" "github.com/arnodel/golua/lib" @@ -21,41 +22,11 @@ var minimalconf = `hilbish.prompt '& '` func luaInit() { l = rt.New(os.Stdout) - l.PushContext(rt.RuntimeContextDef{ - MessageHandler: debuglib.Traceback, - }) - lib.LoadAll(l) - lib.LoadLibs(l, hilbishLoader) - // yes this is stupid, i know - util.DoString(l, "hilbish = require 'hilbish'") + loadLibs(l) - hooks = bait.New(l) - hooks.SetRecoverer(func(event string, handler *bait.Listener, err interface{}) { - fmt.Println("Error in `error` hook handler:", err) - hooks.Off(event, handler) - }) - lib.LoadLibs(l, hooks.Loader) - - // Add Ctrl-C handler - hooks.On("signal.sigint", func(...interface{}) rt.Value { - if !interactive { - os.Exit(0) - } - return rt.NilValue - }) - - lr.rl.RawInputCallback = func(r []rune) { - hooks.Emit("hilbish.rawInput", string(r)) - } - - lib.LoadLibs(l, fs.Loader) - lib.LoadLibs(l, terminal.Loader) - lib.LoadLibs(l, snail.Loader) - - cmds = commander.New(l) - lib.LoadLibs(l, cmds.Loader) - lib.LoadLibs(l, lr.rl.Loader) + yarnPool := yarn.New(yarnloadLibs) + lib.LoadLibs(l, yarnPool.Loader) // Add more paths that Lua can require from _, err := util.DoString(l, "package.path = package.path .. "+requirePaths) @@ -74,6 +45,60 @@ func luaInit() { } } +func loadLibs(r *rt.Runtime) { + r.PushContext(rt.RuntimeContextDef{ + MessageHandler: debuglib.Traceback, + }) + lib.LoadAll(r) + + lib.LoadLibs(r, hilbishLoader) + // yes this is stupid, i know + util.DoString(r, "hilbish = require 'hilbish'") + + hooks = bait.New(r) + hooks.SetRecoverer(func(event string, handler *bait.Listener, err interface{}) { + fmt.Println("Error in `error` hook handler:", err) + hooks.Off(event, handler) + }) + lib.LoadLibs(r, hooks.Loader) + + // Add Ctrl-C handler + hooks.On("signal.sigint", func(...interface{}) rt.Value { + if !interactive { + os.Exit(0) + } + return rt.NilValue + }) + + lr.rl.RawInputCallback = func(rn []rune) { + hooks.Emit("hilbish.rawInput", string(rn)) + } + + lib.LoadLibs(r, fs.Loader) + lib.LoadLibs(r, terminal.Loader) + lib.LoadLibs(r, snail.Loader) + + cmds = commander.New(r) + lib.LoadLibs(r, cmds.Loader) + lib.LoadLibs(l, lr.rl.Loader) +} + +func yarnloadLibs(r *rt.Runtime) { + r.PushContext(rt.RuntimeContextDef{ + MessageHandler: debuglib.Traceback, + }) + lib.LoadAll(r) + + lib.LoadLibs(r, hilbishLoader) + lib.LoadLibs(r, hooks.Loader) + lib.LoadLibs(r, fs.Loader) + lib.LoadLibs(r, terminal.Loader) + lib.LoadLibs(r, snail.Loader) + lib.LoadLibs(r, cmds.Loader) + lib.LoadLibs(l, lr.rl.Loader) + +} + func runConfig(confpath string) { if !interactive { return