// Bytecode interpreter: the MoonBit counterpart of the `peg$parseRule`
// function PEG.js generates with `optimize: "size"`.
///|
priv enum Op {
PushEmptyString
PushUndefined
PushNull
PushFailed
PushEmptyArray
PushCurrPos
Pop
PopCurrPos
PopN
Nip
Append
Wrap
Text
Pluck
If
IfError
IfNotError
WhileNotError
MatchAny
MatchString
MatchStringIc
MatchClass
AcceptN
AcceptString
Expect
LoadSavedPos
UpdateSavedPos
Call
Rule
SilentFailsOn
SilentFailsOff
ExpectNsBegin
ExpectNsEnd
Invalid
}
///|
/// Maps opcode numbers of `ops` to interpreter operations.
fn decode_table(ops : @bytecode.Opcodes) -> Map[Int, Op] {
Map::from_array([
(ops.push_empty_string, PushEmptyString),
(ops.push_undefined, PushUndefined),
(ops.push_null, PushNull),
(ops.push_failed, PushFailed),
(ops.push_empty_array, PushEmptyArray),
(ops.push_curr_pos, PushCurrPos),
(ops.pop, Pop),
(ops.pop_curr_pos, PopCurrPos),
(ops.pop_n, PopN),
(ops.nip, Nip),
(ops.append, Append),
(ops.wrap, Wrap),
(ops.text, Text),
(ops.pluck, Pluck),
(ops.if_, If),
(ops.if_error, IfError),
(ops.if_not_error, IfNotError),
(ops.while_not_error, WhileNotError),
(ops.match_any, MatchAny),
(ops.match_string, MatchString),
(ops.match_string_ic, MatchStringIc),
(ops.match_class, MatchClass),
(ops.accept_n, AcceptN),
(ops.accept_string, AcceptString),
(ops.expect, Expect),
(ops.load_saved_pos, LoadSavedPos),
(ops.update_saved_pos, UpdateSavedPos),
(ops.call, Call),
(ops.rule, Rule),
(ops.silent_fails_on, SilentFailsOn),
(ops.silent_fails_off, SilentFailsOff),
(ops.expect_ns_begin, ExpectNsBegin),
(ops.expect_ns_end, ExpectNsEnd),
])
}
///|
/// A rule's bytecode with opcodes pre-decoded.
priv struct DecodedRule {
name : String
code : Array[Int]
ops : Array[Op]
}
///|
/// Program data shared by all parses of one parser.
priv struct Machine[T] {
program : @bytecode.Program
rules : Array[DecodedRule]
classes : Array[@runtime.ClassMatcher]
static_bindings : Map[String, ActionFn[T]]
initializer : InitFn[T]?
}
///|
/// Per-parse execution: the machine plus parse state and resolved actions.
priv struct Run[T] {
machine : Machine[T]
state : @runtime.State[T]
functions : Array[ActionFn[T]]
/// The machine stack, shared by all rule activations (each works above its
/// own base).
stack : Array[@runtime.Slot[T]]
/// Saved instruction pointers / block ends of the enclosing blocks.
ips : Array[Int]
ends : Array[Int]
}
///|
let empty_expectations : Array[@runtime.Expectation] = []
///|
/// Enters the then/else block of a condition instruction starting at `ip`
/// (with `args_length` operands), saving the current block on `ips`/`ends`.
/// Returns the new `(ip, end)`.
fn branch(
bc : Array[Int],
ip : Int,
end : Int,
cond : Bool,
args_length : Int,
ips : Array[Int],
ends : Array[Int],
) -> (Int, Int) {
let base = args_length + 3
let then_length = bc[ip + base - 2]
let else_length = bc[ip + base - 1]
ends.push(end)
ips.push(ip + base + then_length + else_length)
if cond {
(ip + base, ip + base + then_length)
} else {
(ip + base + then_length, ip + base + then_length + else_length)
}
}
///|
fn[T] Run::call_function(
self : Run[T],
index : Int,
params : ArrayView[String],
args : Array[Value[T]],
) -> Value[T] raise {
let ctx = @runtime.ActionContext::new(self.state, params, args[:])
self.functions[index](ctx)
}
///|
fn[T] Run::parse_rule(self : Run[T], index : Int) -> @runtime.Slot[T] raise {
let machine = self.machine
let program = machine.program
let state = self.state
let rule = machine.rules[index]
let bc = rule.code
let ops = rule.ops
let start_pos = state.curr_pos
let rule_expectations : Array[@runtime.Expectation] = if program.cache {
[]
} else {
empty_expectations
}
if program.trace {
state.trace(RuleEnter, rule.name, start_pos)
}
if program.cache {
if state.cache_lookup(index) is Some(entry) {
if program.trace {
state.trace(
if entry.result is Failed {
RuleFail
} else {
RuleMatch
},
rule.name,
start_pos,
result=entry.result,
)
}
return entry.result
}
}
let stack = self.stack
let ips = self.ips
let ends = self.ends
let stack_base = stack.length()
let ends_base = ends.length()
let mut ip = 0
let mut end = bc.length()
let input = state.input
while true {
while ip < end {
match ops[ip] {
PushEmptyString => {
stack.push(Val(Str("")))
ip += 1
}
PushUndefined => {
stack.push(Val(Undefined))
ip += 1
}
PushNull => {
stack.push(Val(Null))
ip += 1
}
PushFailed => {
stack.push(Failed)
ip += 1
}
PushEmptyArray => {
stack.push(Val(Arr([])))
ip += 1
}
PushCurrPos => {
stack.push(Pos(state.curr_pos))
ip += 1
}
Pop => {
stack.pop() |> ignore
ip += 1
}
PopCurrPos => {
state.curr_pos = @runtime.slot_pos(stack.pop().unwrap())
ip += 1
}
PopN => {
stack.truncate(stack.length() - bc[ip + 1])
ip += 2
}
Nip => {
let v = stack.pop().unwrap()
stack[stack.length() - 1] = v
ip += 1
}
Append => {
let v = @runtime.slot_value(stack.pop().unwrap())
match stack[stack.length() - 1] {
Val(Arr(arr)) => arr.push(v)
_ => abort("APPEND target is not an array")
}
ip += 1
}
Wrap => {
let n = bc[ip + 1]
let base = stack.length() - n
let values = Array::makei(n, i => @runtime.slot_value(stack[base + i]))
stack.truncate(base)
stack.push(Val(Arr(values)))
ip += 2
}
Text => {
let start = @runtime.slot_pos(stack.pop().unwrap())
stack.push(
Val(Str(@runtime.js_substring(input, start, state.curr_pos))),
)
ip += 1
}
Pluck => {
let n = bc[ip + 1]
let k = bc[ip + 2]
let len = stack.length()
let value = if k == 1 {
@runtime.slot_value(stack[len - 1 - bc[ip + 3]])
} else {
Arr(
Array::makei(k, i => {
@runtime.slot_value(stack[len - 1 - bc[ip + 3 + i]])
}),
)
}
stack.truncate(len - n)
stack.push(Val(value))
ip += 3 + k
}
If => {
let (next_ip, next_end) = branch(
bc,
ip,
end,
@runtime.slot_value(stack[stack.length() - 1]).truthy(),
0,
ips,
ends,
)
ip = next_ip
end = next_end
}
IfError => {
let (next_ip, next_end) = branch(
bc,
ip,
end,
stack[stack.length() - 1] is Failed,
0,
ips,
ends,
)
ip = next_ip
end = next_end
}
IfNotError => {
let (next_ip, next_end) = branch(
bc,
ip,
end,
!(stack[stack.length() - 1] is Failed),
0,
ips,
ends,
)
ip = next_ip
end = next_end
}
WhileNotError => {
let body_length = bc[ip + 1]
if !(stack[stack.length() - 1] is Failed) {
ends.push(end)
ips.push(ip)
end = ip + 2 + body_length
ip += 2
} else {
ip += 2 + body_length
}
}
MatchAny => {
let (next_ip, next_end) = branch(
bc,
ip,
end,
input.length() > state.curr_pos,
0,
ips,
ends,
)
ip = next_ip
end = next_end
}
MatchString => {
let (next_ip, next_end) = branch(
bc,
ip,
end,
@runtime.starts_with_at(
input,
state.curr_pos,
program.literals[bc[ip + 1]],
),
1,
ips,
ends,
)
ip = next_ip
end = next_end
}
MatchStringIc => {
let lit = program.literals[bc[ip + 1]]
let chunk = @runtime.js_substr(input, state.curr_pos, lit.length())
let (next_ip, next_end) = branch(
bc,
ip,
end,
@runtime.js_to_lower(chunk) == lit,
1,
ips,
ends,
)
ip = next_ip
end = next_end
}
MatchClass => {
let (next_ip, next_end) = branch(
bc,
ip,
end,
machine.classes[bc[ip + 1]].matches_at(input, state.curr_pos),
1,
ips,
ends,
)
ip = next_ip
end = next_end
}
AcceptN => {
let n = bc[ip + 1]
stack.push(Val(Str(@runtime.js_substr(input, state.curr_pos, n))))
state.curr_pos += n
ip += 2
}
AcceptString => {
let lit = program.literals[bc[ip + 1]]
stack.push(Val(Str(lit)))
state.curr_pos += lit.length()
ip += 2
}
Expect => {
let e = program.expectations[bc[ip + 1]]
state.rule_expects(e)
if program.cache {
rule_expectations.push(e)
}
ip += 2
}
LoadSavedPos => {
state.saved_pos = @runtime.slot_pos(
stack[stack.length() - 1 - bc[ip + 1]],
)
ip += 2
}
UpdateSavedPos => {
state.saved_pos = state.curr_pos
ip += 1
}
Call => {
let f = bc[ip + 1]
let n = bc[ip + 2]
let pc = bc[ip + 3]
let len = stack.length()
let args = Array::makei(pc, i => {
@runtime.slot_value(stack[len - 1 - bc[ip + 4 + i]])
})
let value = self.call_function(
f,
program.functions[f].params[:],
args,
)
stack.truncate(len - n)
stack.push(Val(value))
ip += 4 + pc
}
Rule => {
stack.push(self.parse_rule(bc[ip + 1]))
ip += 2
}
SilentFailsOn => {
state.silent_fails += 1
ip += 1
}
SilentFailsOff => {
state.silent_fails -= 1
ip += 1
}
ExpectNsBegin => {
state.begin()
ip += 1
}
ExpectNsEnd => {
state.end(bc[ip + 1] != 0)
ip += 2
}
Invalid =>
abort(
"Rule #\{index} ('\{rule.name}'), position \{ip}: Invalid opcode \{bc[ip]}.",
)
}
}
if ends.length() > ends_base {
end = ends.pop().unwrap()
ip = ips.pop().unwrap()
} else {
break
}
}
let result = stack[stack_base]
stack.truncate(stack_base)
if program.cache {
state.cache_store(index, start_pos, result, rule_expectations)
}
if program.trace {
state.trace(
if result is Failed {
RuleFail
} else {
RuleMatch
},
rule.name,
start_pos,
result~,
)
}
result
}
///|
fn[T] Machine::run(
self : Machine[T],
input : String,
options : Map[String, Value[T]],
tracer : Tracer[T]?,
) -> Value[T] raise {
let program = self.program
@runtime.drive(
input,
options,
tracer,
allowed_start_rules=program.allowed_start_rules[:],
trace=program.trace,
features=program.features,
run=(state, start) => {
// The initializer runs per parse, after start rule validation. Its
// bindings take precedence over the static ones.
let dynamic = match self.initializer {
Some(init) => init(@runtime.ActionContext::new(state, [][:], [][:]))
None => Map([])
}
let functions = []
for i, f in program.functions {
let binding = match lookup_binding(dynamic, i, f.body) {
Some(impl_) => Some(impl_)
None => lookup_binding(self.static_bindings, i, f.body)
}
match binding {
Some(impl_) => functions.push(impl_)
None =>
raise UnboundCode(
kind=if f.predicate { "predicate" } else { "action" },
code=f.body,
)
}
}
let run = {
machine: self,
state,
functions,
stack: [],
ips: [],
ends: [],
}
state.begin()
run.parse_rule(program.rule_index(start).unwrap())
},
)
}
///|
/// Builds a parser interpreting `program`.
///
/// Each action / predicate code block is implemented by the entry of
/// `actions` keyed by its exact body or trimmed body (see `lookup_binding`).
/// A grammar initializer requires `initializer`, which runs per parse and may
/// supply further bindings. Raises `UnboundCode` for missing bindings (at
/// parse time for bindings the initializer is expected to provide).
pub fn[T] Parser::from_program(
program : @bytecode.Program,
actions? : Map[String, ActionFn[T]] = {},
initializer? : InitFn[T],
) -> Parser[T] raise UnboundCode {
if program.initializer is Some(code) && initializer is None {
raise UnboundCode(kind="initializer", code~)
}
if initializer is None {
for i, f in program.functions {
if lookup_binding(actions, i, f.body) is None {
raise UnboundCode(
kind=if f.predicate { "predicate" } else { "action" },
code=f.body,
)
}
}
}
let table = decode_table(program.opcodes)
let rules = program.rules.map(r => DecodedRule::{
name: r.name,
code: r.bytecode,
ops: r.bytecode.map(b => {
match table.get(b) {
Some(op) => op
None => Invalid
}
}),
})
let classes = program.classes.map(c => {
@runtime.ClassMatcher::new(
c.parts[:],
inverted=c.inverted,
ignore_case=c.ignore_case,
)
})
let machine = {
program,
rules,
classes,
static_bindings: actions,
initializer,
}
{
parse_impl: (input, options, tracer) => machine.run(input, options, tracer),
program: Some(program),
}
}