///|
/// A small Prolog interpreter: loads an optional program file and answers
/// queries given with `--query` or read from standard input.
#derive(Debug)
suberror CliError {
Msg(String)
}
///|
async fn main {
let code = run() catch {
CliError::Msg(msg) => {
ignore(@stdio.stderr.write("\{msg}\n") catch { _ => () })
@sys.exit(2)
fail("unreachable")
}
}
@sys.exit(code)
}
///|
async fn run() -> Int32 raise CliError {
let matches = @argparse.parse(
@argparse.Command(
"prolog",
about="A small Prolog interpreter in MoonBit.",
flags=[
@argparse.FlagArg(
"dfs",
long="dfs",
action=@argparse.FlagAction::SetTrue,
about="Plain depth-first search instead of iterative deepening.",
),
],
options=[
@argparse.OptionArg(
"query",
short='q',
long="query",
action=@argparse.OptionAction::Append,
about="Run one query and exit instead of reading stdin (repeatable).",
),
@argparse.OptionArg(
"program",
long="program",
about="Prolog program text (alternative to a program file).",
),
@argparse.OptionArg(
"depth",
long="depth",
default_values=["32"],
about="Maximum resolution depth (default 32).",
),
@argparse.OptionArg(
"max-steps",
long="max-steps",
default_values=["1000000"],
about="Maximum inference steps per query (default 1000000).",
),
@argparse.OptionArg(
"max-solutions",
long="max-solutions",
default_values=["1000"],
about="Maximum solutions per query (default 1000).",
),
],
positionals=[
@argparse.PositionArg("file", about="Prolog program file (optional)."),
],
),
) catch {
e => raise CliError::Msg(e.to_string())
}
let file = match matches.values.get("file") {
Some([f, ..]) => f
_ => ""
}
let opts = opts_from_matches(matches)
let text = if file == "" {
match matches.values.get("program") {
Some([p, ..]) => p
_ => ""
}
} else {
@fs.read_file(file).text() catch {
e => raise CliError::Msg("cannot read \{file}: \{e.to_string()}")
}
}
let program = @lib.Program::parse(text) catch {
e => raise CliError::Msg(e.message())
}
let queries = match matches.values.get("query") {
Some(qs) => qs
_ => []
}
if queries.is_empty() {
repl(program, opts)
0
} else {
// Run every supplied query in order; a failing query is reported but
// does not stop the remaining ones from running.
let mut failed = false
for q in queries {
if run_query(program, opts, q) != 0 {
failed = true
}
}
if failed {
1
} else {
0
}
}
}
///|
fn opts_from_matches(
matches : @argparse.Matches,
) -> @lib.Options raise CliError {
let base = @lib.Options::new()
fn int_opt(name : String, default : Int32) -> Int32 raise CliError {
match matches.values.get(name) {
Some([v, ..]) => {
let x : Int32 = @string.from_str(v) catch {
_ => raise CliError::Msg("invalid integer for --\{name}: \{v}")
}
if x < 0 {
raise CliError::Msg("--\{name} must be non-negative")
}
x
}
_ => default
}
}
@lib.Options::make(
int_opt("depth", base.max_depth),
!matches.flags.get("dfs").unwrap_or(false),
int_opt("max-steps", base.max_steps),
int_opt("max-solutions", base.max_solutions),
)
}
///|
/// Write to stdout, reporting I/O failure instead of raising.
async fn stdout_write(text : String) -> Bool noraise {
@stdio.stdout.write(text) catch {
_ => return false
}
true
}
///|
/// Write to stderr, reporting I/O failure instead of raising.
async fn stderr_write(text : String) -> Bool noraise {
@stdio.stderr.write(text) catch {
_ => return false
}
true
}
///|
/// Run one query and print its output and answers. Returns 0 on success and
/// 1 on parse/evaluation/I/O failure.
async fn run_query(
program : @lib.Program,
opts : @lib.Options,
text : String,
) -> Int32 noraise {
let result = program.query_with(text, opts) catch {
e => {
ignore(stderr_write("error: \{e.message()}\n"))
return 1
}
}
let mut ok = true
if result.output != "" {
if !stdout_write(result.output) {
ok = false
}
}
if result.solutions.is_empty() {
if !stdout_write("false.\n") {
ok = false
}
} else {
for line in result.answer_lines() {
if !stdout_write("\{line}\n") {
ok = false
}
}
}
let note = match result.completion {
@lib.Completion::Complete => ""
@lib.Completion::Depth => "% note: search truncated by depth limit"
@lib.Completion::Steps =>
"% note: search truncated by inference-step budget"
@lib.Completion::Solutions => "% note: search truncated by solution limit"
@lib.Completion::IncompleteNegation =>
"% note: result is incomplete (bounded negation)"
}
if note != "" {
if !stderr_write("\{note}\n") {
ok = false
}
}
if ok {
0
} else {
1
}
}
///|
/// True when `text` is a complete statement: brackets balanced, not inside a
/// quoted string or comment, and the last significant character is `.`.
fn statement_complete(text : String) -> Bool {
let n = text.length()
let mut i = 0
let mut depth = 0
let mut in_squote = false
let mut in_dquote = false
let mut saw_dot = false
while i < n {
let c = text[i]
if in_squote {
if c == '\\' && i + 1 < n {
i += 2
continue
}
if c == '\'' {
in_squote = false
}
i += 1
continue
}
if in_dquote {
if c == '\\' && i + 1 < n {
i += 2
continue
}
if c == '"' {
in_dquote = false
}
i += 1
continue
}
if c == '%' {
while i < n && text[i] != '\n' {
i += 1
}
continue
}
if c == '/' && i + 1 < n && text[i + 1] == '*' {
i += 2
while i + 1 < n && !(text[i] == '*' && text[i + 1] == '/') {
i += 1
}
i += 2
continue
}
if c == '\'' {
in_squote = true
saw_dot = false
i += 1
continue
}
if c == '"' {
in_dquote = true
saw_dot = false
i += 1
continue
}
if c == '(' || c == '[' {
depth += 1
saw_dot = false
i += 1
continue
}
if c == ')' || c == ']' {
depth -= 1
saw_dot = false
i += 1
continue
}
if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
i += 1
continue
}
if c == '.' {
saw_dot = true
i += 1
continue
}
saw_dot = false
i += 1
}
depth == 0 && !in_squote && !in_dquote && saw_dot
}
///|
/// Interactive mode: read queries from standard input. A statement is complete
/// only when its brackets are balanced and its last significant character is
/// `.`, so a query may span several lines. `halt.` quits.
async fn repl(program : @lib.Program, opts : @lib.Options) -> Unit noraise {
let mut buffer : Array[String] = []
while true {
let line = @stdio.stdin.read_until("\n") catch { _ => None }
match line {
None => {
if !buffer.is_empty() {
ignore(run_query(program, opts, buffer.join("\n")))
}
break
}
Some(l) => {
let trimmed = l.trim()
if trimmed == "" {
continue
}
if trimmed == "halt" || trimmed == "halt." {
break
}
buffer.push(l)
if statement_complete(buffer.join("\n")) {
ignore(run_query(program, opts, buffer.join("\n")))
buffer = []
}
}
}
}
}