Skip to content

Commit

Permalink
_js: add a non-blocking wrapper of Interactive
Browse files Browse the repository at this point in the history
JS only has a single thread, meaning that if the interactive parser
blocks on a call, the only way to run any other code in the meantime is
to run it as part of the "read" call.

This is understandably clunky, as the API was designed for concurrent
languages. However, with about twenty lines of code we can offer an API
that never blocks to JS, which is more usable for real programs.

It does re-parse chunks of incomplete statements, but those shouldn't
get very large. Adding a non-blocking Interactive variant to the
original parser would be too much of a refactor, since it's a recursive
descent parser. It can't be stopped and resumed, as the parse state is
in the call stack.
  • Loading branch information
mvdan committed Feb 25, 2019
1 parent d7d5f5a commit 1c6516f
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 0 deletions.
30 changes: 30 additions & 0 deletions _js/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ func (r streamReader) Read(p []byte) (n int, err error) {

type jsParser struct {
syntax.Parser

accumulated []*syntax.Stmt
incomplete bytes.Buffer
}

func adaptReader(src *js.Object) io.Reader {
Expand Down Expand Up @@ -151,6 +154,33 @@ func (p *jsParser) Interactive(src *js.Object, jsFn func([]*js.Object) bool) {
}
}

func (p *jsParser) InteractiveStep(line string) []*syntax.Stmt {
// pick up previous chunks of the incomplete statement
r := strings.NewReader(p.incomplete.String() + line)
lastEnd := uint(0)
err := p.Parser.Interactive(r, func(stmts []*syntax.Stmt) bool {
if len(stmts) > 0 {
// don't re-parse finished statements
lastEnd = stmts[len(stmts)-1].End().Offset()
}
p.accumulated = append(p.accumulated, stmts...)
return false
})
if syntax.IsIncomplete(err) {
// starting or continuing an incomplete statement
p.incomplete.WriteString(line[lastEnd:])
return p.accumulated
}
// complete; empty both fields and return
p.incomplete.Reset()
if err != nil {
throw(err)
}
acc := p.accumulated
p.accumulated = p.accumulated[:0]
return acc
}

type jsPrinter struct {
*syntax.Printer
}
Expand Down
32 changes: 32 additions & 0 deletions _js/testmain.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,38 @@ const printer = syntax.NewPrinter()
assert.deepEqual(gotCallbacks, wantCallbacks)
}

{
// using the parser interactively with steps
const lines = [
"foo\n",
"bar; baz\n",
"\n",
"foo; 'incom\n",
" \n",
"plete'\n",
]
const wantResults = [
{"count": 1, "incomplete": false},
{"count": 2, "incomplete": false},
{"count": 0, "incomplete": false},
{"count": 1, "incomplete": true},
{"count": 1, "incomplete": true},
{"count": 2, "incomplete": false},
]
var gotResults = []
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
var want = wantResults[i]

var stmts = parser.InteractiveStep(line)
gotResults.push({
"count": stmts.length,
"incomplete": parser.Incomplete(),
})
}
assert.deepEqual(gotResults, wantResults)
}

{
// splitting brace expressions
const parser = syntax.NewParser()
Expand Down

0 comments on commit 1c6516f

Please sign in to comment.