-
-
Notifications
You must be signed in to change notification settings - Fork 120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add custom iterator function support which enables implementing a REPL in jq #67
Conversation
Noticed now that tests are failing, ill have a look at it later. Looks like a |
execute.go
Outdated
@@ -149,37 +149,90 @@ loop: | |||
goto loop | |||
} | |||
case opcall: | |||
if backtrack { | |||
break loop | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You cannot remove these 3 lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok thanks. Think the reason i moved it into only be done for non-iterator opcall case was to try mimic how opeach
case does things but i probably got it wrong. Will have to think more about it. Also don't like that there is some duplication the way I did this now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, you're right. I missed you moved it to L181.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But i'm guessing the unit test fail might be because i do a pop too much in some error case, haven't look closer at it yet.
Do you want me to try make this proof-of-concept-PR into something that could be mergeable? Now i just commented out some things i don't understand how it work yet and did some ugly code duplication to just see if i could make it work. Wanted to get your feedback if i'm on the correct track and if this feature would be something you want to have. Maybe you want to work on implementing this yourself? i'm happy to help either way. |
First of all, thank you for various feedbacks, and you are the first one trying to dive deeply into the gojq interpreter. I don't decided yet but this kind of feature extension is likely to be declined. Pushing an iterator to the stack breaks various assumptions in the implementation. Also I am wondering how much this is useful in other usages than |
Yes very much understand that, and thanks of offering advice, will most likely need it. Do you have any ideas how a implementation would look like that don't keep a iterator on the stack? or maybe it would be ok but some code assuming things will have to be changed? Was thinking today what other uses than
For example could type sliceIter struct{ c []interface{} }
func (si *sliceIter) Next() (interface{}, bool) {
if len(si.c) == 0 {
return nil, false
}
e := si.c[0]
if err, ok := e.(error); ok {
return err, false
}
si.c = si.c[1:]
return e, true
}
func each(c interface{}, a []interface{}) interface{} {
switch c := c.(type) {
case []interface{}:
return &sliceIter{c}
case map[string]interface{}:
xs := make([][2]interface{}, len(c))
var i int
for k, v := range c {
xs[i] = [2]interface{}{k, v}
i++
}
sort.Slice(xs, func(i, j int) bool {
return xs[i][0].(string) < xs[j][0].(string)
})
ss := make([]interface{}, len(c))
for i, v := range xs {
ss[i] = v[1]
}
return &sliceIter{ss}
default:
panic("unreachable") // TODO: ?
}
} Now i realized to make this work the function iterator interface probably can't use Again thanks for taking your time and I really enjoy working with the gojq code. |
Hi again, i tried your suggestion to use a jq function to wrap an internal function to implement a generator. Seems to work fine, fascinatingly simple :) I did these changes: Renamed internal func (preludeLoader) LoadInitModules() ([]*gojq.Query, error) {
replSrc := `
def eval($e): _eval($e)[];
def repl:
def _wrap: if (. | type) != "array" then [.] end;
def _repl:
try read("> ") as $e |
(try (.[] | eval($e)) catch . | print),
_repl;
_wrap | _repl;
`
gq, err := gojq.Parse(replSrc)
if err != nil {
return nil, err
}
return []*gojq.Query{gq}, nil
} func eval(c interface{}, a []interface{}) interface{} {
src, ok := a[0].(string)
if !ok {
return fmt.Errorf("%v: src is not a string", a[0])
}
iter, err := replRun(c, src)
if err != nil {
return err
}
var vs []interface{}
for {
v, ok := iter.Next()
if !ok {
break
}
vs = append(vs, v)
if _, ok := v.(error); ok {
break
}
}
return vs
} Trying to come up with ways it might behave differently to returning an iterator. As long as the eval expression is "pure" i think it should not matter that we empty the iterator and then return all values instead of doing one at a time. Is it correct to handle error just as any value but stop iterating? |
One difference because the generator is not "lazy" when wrapped is that you can't use a internal function to generate a unknown number of values, for example a infinite range or to read inputs etc. Maybe a bit of a edge case but could be worth to document? |
The problem of handling |
For my use case of But I do feel the idea to support internal iterator function is quite compelling and might be useful, and as i mention maybe it can be used to implement some builtins things like Should i continue look into adding |
Just noticed that |
I noticed in #39 that you mentioned capture of variables. By that did you mean the ability to somehow get the environment from a evaluated expression? i've thought about somehow support something like this:
Not sure how to accesses to the environment in a nice way but providing an a environment i think could be a second argument to |
BTW i added jq support to github linguist github-linguist/linguist#5233 so soon jq files should be properly highlighted (and also not detected as JSONiq) which also should add support for jq code blocks in markdown like this: ```jq def f: 123; ``` |
a505d05
to
7d92d9a
Compare
Hi again, got the idea to extend Some questions and TODOs: Not a good idea to separate function/iterator? could wrap all functions as one value iterators but might impact performance? Should WithIterator callback use return type Keep track of path? currently iterators give 0 as path for each value. Feel i don't really understand that part of the code yet. Remove REPL or add to CLI? Have a Useful to provide Test for Update debugger? |
@@ -217,10 +217,17 @@ loop: | |||
break loop | |||
} | |||
backtrack = false | |||
|
|||
var xc [2]interface{} | |||
var xv interface{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea was to refactor so that the pus/fork below can be shared. xc
is current and xn
is next value for opeach
.
Can I just voice my support of a repl feature! Great idea! |
Noticed that |
TODO: NewSliceIter helper? less confusion about reference or not Make execte.go debug and Iter code nicer Not a good idea to separate function/iterator? could wrap all functions as one value iterators but might impact performance? Keep track of path? currently iterator give 0 as path Remove repl or add to cli?
Added |
I'm sorry for keeping this pull request open for months. I wasn't sure the correctness of the patch, especially in execute.go, and whether or not accepting the iterator utilities and repl code. I have included to the main branch. I really appreciate your work and committed with adding you as a co-author.
|
No worries
Both good ideas i think, API feels cleaner
Understand, i mostly included it to show my use case. I've been thinking how to handle In my version i went for the pass-thru but now i got second thought maybe it's better to do something like this: func eval(c interface{}, a []interface{}) gojq.Iter {
src, ok := a[0].(string)
if !ok {
return gojq.NewIter(fmt.Errorf("%v: src is not a string", a[0]))
}
iter, err := replRun(c, src)
if err != nil {
return gojq.NewIter(err)
}
return IterFn(func() (interface{}, bool) {
for {
v, ok := iter.Next()
if _, ok := v.([2]interface{}); ok {
// custom debug handling here
continue
}
return v, ok
}
})
} Again, thanks for taking the time to review and work on this |
Hmm, it seems that it was a mistake to implementing |
Aha ok you mean use |
Right, library users can implement them with the option to choose how they print the value. Called order does not change. |
Hi! this is more of a feature request than a PR but I choose to use a PR to show some proof of concept code.
Background is that i'm working on tool based on gojq that has a interactive CLI REPL interface. The REPL used to be implemented in go with quite a lot of messy code to make it "feel" like jq. Some days I realized that much of this could probably be solved if the REPL itself was written in jq. After some thinking i realized that adding support for custom iterator functions would enable implementing
eval
as custom function.With
read
andprint
as custom functions you can implement a REPL like this:A bit more complicated than
def repl: read | eval(.) | print, repl; repl
to make it more user friendly.Example usage showing basic expressions, nested REPL and errors:
Some implementation notes:
repl
takes an array as input and will array wrap non-array values. This feels natural but maybe there are better solutions?1, 2, 3 | repl
. The current behavior to give multiple REPLs feels ok i think.env.paths
in*env.Next()
. I guess it's related to assignment andpaths
? i haven't looked into how this could affect it.What do you think? could be useful?