diff --git a/env/env.go b/env/env.go index e34f8316..f2023c25 100755 --- a/env/env.go +++ b/env/env.go @@ -4,6 +4,9 @@ import ( "fmt" "sort" "strings" + "sync" + + "github.com/fsnotify/fsnotify" ) /* type Envi interface { @@ -244,7 +247,7 @@ func (e *RyeCtx) Get2(word int) (Object, bool, *RyeCtx) { func (e *RyeCtx) Set(word int, val Object) Object { if _, exists := e.state[word]; exists { - return NewError("Can't set already set word, try using modword") + return NewError("Can't set already set word, try using modword! FIXME !") } else { e.state[word] = val return val @@ -296,6 +299,8 @@ type ProgramState struct { InErrHandler bool ScriptPath string // holds the path to the script that is being imported (doed) currently WorkingPath string // holds the path to CWD (can be changed in program with specific functions) + AllowMod bool + LiveObj *LiveEnv } func NewProgramState(ser TSeries, idx *Idxs) *ProgramState { @@ -317,6 +322,8 @@ func NewProgramState(ser TSeries, idx *Idxs) *ProgramState { false, "", "", + false, + NewLiveEnv(), } return &ps } @@ -340,6 +347,8 @@ func NewProgramStateNEW() *ProgramState { false, "", "", + false, + NewLiveEnv(), } return &ps } @@ -368,3 +377,54 @@ func SetValue(ps *ProgramState, word string, val Object) { } } } + +// LiveEnv -- a experiment in live realoading + +type LiveEnv struct { + Active bool + Watcher *fsnotify.Watcher + PsMutex sync.Mutex + Updates []string +} + +func NewLiveEnv() *LiveEnv { + watcher, err := fsnotify.NewWatcher() + if err != nil { + fmt.Println("Error creating watcher:", err) + return nil + } + // defer watcher.Close() + + // Watch current directory for changes in any Go source file (*.go) + + liveEnv := &LiveEnv{true, watcher, sync.Mutex{}, make([]string, 0)} + + go func() { + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + // fmt.Println("LiveEnv file changed:", event.Name) + liveEnv.PsMutex.Lock() + liveEnv.Updates = append(liveEnv.Updates, event.Name) + liveEnv.PsMutex.Unlock() + } + case err := <-watcher.Errors: + fmt.Println("LiveEnv error watching files:", err) + } + } + }() + + return liveEnv +} + +func (le *LiveEnv) Add(file string) { + err := le.Watcher.Add(".") + if err != nil { + fmt.Println("LiveEnv: Error adding directory to watch:", err) + } +} + +func (le *LiveEnv) ClearUpdates() { + le.Updates = make([]string, 0) +} diff --git a/evaldo/builtins.go b/evaldo/builtins.go index 3999c8b5..62e52808 100644 --- a/evaldo/builtins.go +++ b/evaldo/builtins.go @@ -73,6 +73,7 @@ func MakeRyeError(env1 *env.ProgramState, val env.Object, er *env.Error) *env.Er return env.NewError4(int(val.Value), "", er, nil) case env.Block: // todo .. make Error type .. make error construction micro dialect, return the error wrapping error that caused it // TODO -- this is only temporary it takes numeric value as first and string as second arg + // TODONOW implement the dialect code := val.Series.Get(0) message := val.Series.Get(1) if code.Type() == env.IntegerType && message.Type() == env.StringType { @@ -332,6 +333,44 @@ func (s RyeListSort) Less(i, j int) bool { return greaterThanNew(env.ToRyeValue(s[j]), env.ToRyeValue(s[i])) } +func LoadScriptLocalFile(ps *env.ProgramState, s1 env.Uri) (env.Object, string) { + var str string + fileIdx, _ := ps.Idx.GetIndex("file") + fullpath := filepath.Join(filepath.Dir(ps.ScriptPath), s1.GetPath()) + if s1.Scheme.Index == fileIdx { + b, err := os.ReadFile(fullpath) + if err != nil { + return MakeBuiltinError(ps, err.Error(), "import"), ps.ScriptPath + } + str = string(b) // convert content to a 'string' + } + script_ := ps.ScriptPath + ps.ScriptPath = fullpath + block_ := loader.LoadStringNEW(str, false, ps) + return block_, script_ +} + +func EvaluateLoadedValue(ps *env.ProgramState, block_ env.Object, script_ string, allowMod bool) env.Object { + switch block := block_.(type) { + case env.Block: + ser := ps.Ser + ps.Ser = block.Series + ps.AllowMod = allowMod + EvalBlock(ps) + ps.AllowMod = false + ps.Ser = ser + return ps.Res + case env.Error: + ps.ScriptPath = script_ + ps.ErrorFlag = true + return MakeBuiltinError(ps, block.Message, "import") + default: + fmt.Println(block) + panic("Not block and not error in import builtin.") // TODO -- Think how best to handle this + // return env.Void{} + } +} + var ShowResults bool var builtins = map[string]*env.Builtin{ @@ -376,6 +415,8 @@ var builtins = map[string]*env.Builtin{ return MakeBuiltinError(ps, err.Error(), "to-integer") } return *env.NewInteger(int64(iValue)) + case env.Decimal: + return *env.NewInteger(int64(addr.Value)) default: return MakeArgError(ps, 1, []env.Type{env.StringType}, "to-integer") } @@ -1580,20 +1621,24 @@ var builtins = map[string]*env.Builtin{ Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { switch s1 := arg0.(type) { case env.Uri: - var str string - fileIdx, _ := ps.Idx.GetIndex("file") - fullpath := filepath.Join(filepath.Dir(ps.ScriptPath), s1.GetPath()) - if s1.Scheme.Index == fileIdx { - b, err := os.ReadFile(fullpath) - if err != nil { - return MakeBuiltinError(ps, err.Error(), "import") - } - str = string(b) // convert content to a 'string' - } - script_ := ps.ScriptPath - ps.ScriptPath = fullpath - block_ := loader.LoadStringNEW(str, false, ps) - switch block := block_.(type) { + block_, script_ := LoadScriptLocalFile(ps, s1) + /* + var str string + fileIdx, _ := ps.Idx.GetIndex("file") + fullpath := filepath.Join(filepath.Dir(ps.ScriptPath), s1.GetPath()) + if s1.Scheme.Index == fileIdx { + b, err := os.ReadFile(fullpath) + if err != nil { + return MakeBuiltinError(ps, err.Error(), "import") + } + str = string(b) // convert content to a 'string' + } + script_ := ps.ScriptPath + ps.ScriptPath = fullpath + block_ := loader.LoadStringNEW(str, false, ps) + */ + ps.Res = EvaluateLoadedValue(ps, block_, script_, false) + /* switch block := block_.(type) { case env.Block: ser := ps.Ser ps.Ser = block.Series @@ -1606,7 +1651,25 @@ var builtins = map[string]*env.Builtin{ default: fmt.Println(block) panic("Not block and not error in import builtin.") // TODO -- Think how best to handle this - } + } */ + ps.ScriptPath = script_ + //ps = env.AddToProgramState(ps, block.Series, genv) + return ps.Res + default: + return MakeArgError(ps, 1, []env.Type{env.UriType}, "import") + } + }, + }, + + "import\\live": { // ** + Argsn: 1, + Doc: "Imports a file, loads and does it from script local path.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch s1 := arg0.(type) { + case env.Uri: + block_, script_ := LoadScriptLocalFile(ps, s1) + ps.Res = EvaluateLoadedValue(ps, block_, script_, false) + ps.LiveObj.Add(s1.GetPath()) // add to watcher ps.ScriptPath = script_ //ps = env.AddToProgramState(ps, block.Series, genv) return ps.Res @@ -1648,7 +1711,78 @@ var builtins = map[string]*env.Builtin{ }, }, - "load-sig": { + // TODO -- refactor load variants so they use common function LoadString and LoadFile + + "load\\mod": { // ** + Argsn: 1, + Doc: "Loads a string into Rye values. During load it allows modification of words.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch s1 := arg0.(type) { + case env.String: + block, _ := loader.LoadString(s1.Value, false) + //ps = env.AddToProgramState(ps, block.Series, genv) + return block + case env.Uri: + var str string + fileIdx, _ := ps.Idx.GetIndex("file") + if s1.Scheme.Index == fileIdx { + b, err := os.ReadFile(s1.GetPath()) + if err != nil { + return makeError(ps, err.Error()) + } + str = string(b) // convert content to a 'string' + } + scrip := ps.ScriptPath + ps.AllowMod = true + ps.ScriptPath = s1.GetPath() + block := loader.LoadStringNEW(str, false, ps) + ps.AllowMod = false + ps.ScriptPath = scrip + //ps = env.AddToProgramState(ps, block.Series, genv) + return block + default: + ps.FailureFlag = true + return env.NewError("Must be string or file TODO") + } + }, + }, + + "load\\live": { // ** + Argsn: 1, + Doc: "Loads a string into Rye values. During load it allows modification of words.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch s1 := arg0.(type) { + case env.String: + block, _ := loader.LoadString(s1.Value, false) + //ps = env.AddToProgramState(ps, block.Series, genv) + return block + case env.Uri: + var str string + fileIdx, _ := ps.Idx.GetIndex("file") + if s1.Scheme.Index == fileIdx { + b, err := os.ReadFile(s1.GetPath()) + ps.LiveObj.Add(s1.GetPath()) // add to watcher + if err != nil { + return makeError(ps, err.Error()) + } + str = string(b) // convert content to a 'string' + } + scrip := ps.ScriptPath + ps.AllowMod = true + ps.ScriptPath = s1.GetPath() + block := loader.LoadStringNEW(str, false, ps) + ps.AllowMod = false + ps.ScriptPath = scrip + //ps = env.AddToProgramState(ps, block.Series, genv) + return block + default: + ps.FailureFlag = true + return env.NewError("Must be string or file TODO") + } + }, + }, + + "load\\sig": { Argsn: 1, Doc: "Checks the signature, if OK then loads a string into Rye values.", Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { diff --git a/evaldo/builtins_io.go b/evaldo/builtins_io.go index e9d1e259..6ebcc483 100755 --- a/evaldo/builtins_io.go +++ b/evaldo/builtins_io.go @@ -525,7 +525,7 @@ func __https_request__do(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, case env.Native: client := &http.Client{} resp, err := client.Do(req.Value.(*http.Request)) - defer resp.Body.Close() + defer resp.Body.Close() // TODO -- comment this and figure out goling bodyclose if err != nil { return MakeBuiltinError(ps, err.Error(), "__https_request__do") } diff --git a/evaldo/evaldo.go b/evaldo/evaldo.go index c189a8b1..b996e520 100644 --- a/evaldo/evaldo.go +++ b/evaldo/evaldo.go @@ -300,11 +300,15 @@ func MaybeEvalOpwordOnRight(nextObj env.Object, ps *env.ProgramState, limited bo } //ProcOpword(nextObj, es) idx := opword.Index - ok := ps.Ctx.SetNew(idx, ps.Res, ps.Idx) - if !ok { - ps.Res = *env.NewError("Can't set already set word " + ps.Idx.GetWord(idx) + ", try using modword") - ps.ErrorFlag = true - return ps + if ps.AllowMod { + ps.Ctx.Mod(idx, ps.Res) + } else { + ok := ps.Ctx.SetNew(idx, ps.Res, ps.Idx) + if !ok { + ps.Res = *env.NewError("Can't set already set word " + ps.Idx.GetWord(idx) + ", try using modword") + ps.ErrorFlag = true + return ps + } } ps.Ser.Next() ps.SkipFlag = false @@ -538,11 +542,15 @@ func EvalSetword(ps *env.ProgramState, word env.Setword) *env.ProgramState { // es1 := EvalExpression(es) ps1, _ := EvalExpressionInj(ps, nil, false) idx := word.Index - ok := ps1.Ctx.SetNew(idx, ps1.Res, ps.Idx) - if !ok { - ps.Res = *env.NewError("Can't set already set word " + ps.Idx.GetWord(idx) + ", try using modword") - ps.FailureFlag = true - ps.ErrorFlag = true + if ps.AllowMod { + ps1.Ctx.Mod(idx, ps.Res) + } else { + ok := ps1.Ctx.SetNew(idx, ps1.Res, ps.Idx) + if !ok { + ps.Res = *env.NewError("Can't set already set word " + ps.Idx.GetWord(idx) + ", try using modword") + ps.FailureFlag = true + ps.ErrorFlag = true + } } return ps } diff --git a/evaldo/repl.go b/evaldo/repl.go index 3c7b1daa..42d6831a 100644 --- a/evaldo/repl.go +++ b/evaldo/repl.go @@ -194,6 +194,15 @@ func DoRyeRepl(es *env.ProgramState, showResults bool) { if code, err := line.Prompt(prompt); err == nil { // strip comment + es.LiveObj.PsMutex.Lock() + for _, update := range es.LiveObj.Updates { + fmt.Println("\033[35m((Reloading " + update + "))\033[0m") + block_, script_ := LoadScriptLocalFile(es, *env.NewUri1(es.Idx, "file://"+update)) + es.Res = EvaluateLoadedValue(es, block_, script_, true) + } + es.LiveObj.ClearUpdates() + es.LiveObj.PsMutex.Unlock() + multiline := len(code) > 1 && code[len(code)-1:] == " " comment := regexp.MustCompile(`\s*;`) diff --git a/examples/xmlprint/xmlprint.rye b/examples/xmlprint/xmlprint.rye index 44471982..f5842586 100644 --- a/examples/xmlprint/xmlprint.rye +++ b/examples/xmlprint/xmlprint.rye @@ -1,5 +1,5 @@ -doc +doc! `This is a simple xmlprint dialect that exposes just one function xmlprint It prints the XML, handles the line padding and check if the tags match` @@ -46,7 +46,7 @@ private { } ; Only this function will get returned and set to xmlgen word - fn\par { values block "Accepts dict of values and a block of xtags extags and code" } current-ctx { process block values 0 'no } + fn\par { values block "Accepts a value that gets injected in code and a block of xtags extags and code" } current-ctx { process block values 0 'no } } :xmlprint diff --git a/go.mod b/go.mod index 921e7f24..b752ba32 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( github.com/blevesearch/zapx/v15 v15.3.13 // indirect github.com/blevesearch/zapx/v16 v16.0.12 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect diff --git a/go.sum b/go.sum index 8251c920..86d0cc37 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,8 @@ github.com/drewlanenga/govector v0.0.0-20220726163947-b958ac08bc93 h1:2VXZHsypUG github.com/drewlanenga/govector v0.0.0-20220726163947-b958ac08bc93/go.mod h1:AbP/uRrjZFATEwl0P2DHePteIMZRWHEJBWBmMmLdCkk= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E=