-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Major refactor of CLI parsing, using dry-cli gem
Lots of refactoring, mostly to move each sub-command into its own class.
- Loading branch information
Showing
13 changed files
with
247 additions
and
163 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,168 +1,20 @@ | ||
require "extensions/argf" | ||
require "extensions/class" | ||
|
||
require "stone/version" | ||
require "stone/verification/suite" | ||
require "stone/top" | ||
require "stone/ast/error" | ||
|
||
# Load all the sub-languages, then determine the highest-level sub-language (it'll have the most ancestors). | ||
Dir[File.join(__dir__, "language", "*.rb")].sort.each do |file| | ||
require file | ||
end | ||
DEFAULT_LANGUAGE = Stone::Language::Base.descendants.max_by { |lang| lang.ancestors.size } | ||
|
||
require "readline" | ||
require "kramdown" | ||
require "dry/cli" | ||
|
||
|
||
module Stone | ||
module CLI | ||
|
||
class CLI | ||
extend Dry::CLI::Registry | ||
|
||
def self.run | ||
new.run | ||
end | ||
|
||
def run | ||
if options.include?("--version") | ||
puts "Stone version #{Stone::VERSION}" | ||
exit 0 | ||
elsif self.respond_to?("run_#{subcommand}", true) | ||
__send__("run_#{subcommand}") | ||
exit 0 | ||
else | ||
puts "Don't know the '#{subcommand}' subcommand." | ||
exit 1 | ||
end | ||
end | ||
|
||
private def language | ||
@language ||= DEFAULT_LANGUAGE.new | ||
end | ||
|
||
private def run_parse | ||
each_input_file do |input| | ||
puts language.parse(input) | ||
rescue Parslet::ParseFailed => e | ||
puts e.parse_failure_cause.ascii_tree | ||
exit 1 | ||
end | ||
end | ||
|
||
private def run_eval | ||
each_input_file do |input| | ||
top_context = Stone::Top.context | ||
puts language.ast(input).map{ |node| | ||
node.evaluate(top_context) | ||
}.compact | ||
rescue Parslet::ParseFailed => e | ||
puts e.parse_failure_cause.ascii_tree | ||
exit 1 | ||
end | ||
end | ||
|
||
private def run_repl | ||
puts "Stone REPL" | ||
while (input = Readline.readline("#> ", true)) | ||
repl_1_line(input, top_context) | ||
end | ||
end | ||
|
||
private def repl_1_line(line, context) | ||
ast = language.ast(line, single_line: true) | ||
case result = ast.evaluate(context) | ||
when AST::Error | ||
puts "#! #{result}" | ||
when AST::Value | ||
puts "#= #{result}" | ||
end | ||
rescue Parslet::ParseFailed => e | ||
puts e.parse_failure_cause.ascii_tree | ||
end | ||
|
||
private def run_verify | ||
suite = Stone::Verification::Suite.new(debug: debug_option?) | ||
each_input_file do |input| | ||
suite.run(input) do | ||
language.ast(input) | ||
rescue Parslet::ParseFailed => e | ||
suite.add_failure(input, Stone::AST::Error.new("ParseError", e.parse_failure_cause)) | ||
[] | ||
end | ||
end | ||
suite.complete | ||
end | ||
|
||
private def options # rubocop:disable Metrics/AbcSize, Metrics/MethodLength | ||
return @options if @options | ||
@options = [] | ||
while ARGV[0] =~ /^--/ | ||
@options << ARGV[0] | ||
ARGV.shift | ||
# TODO: I should really use an options-parsing library here. | ||
if @options.last == "--grammar" # rubocop:disable Style/Next | ||
grammar = ARGV[0].capitalize | ||
if Stone::Language.const_defined?(grammar.to_sym) | ||
@language = Stone::Language.const_get(grammar.to_sym).new | ||
else | ||
puts "Don't know the #{grammar} sub-language." | ||
exit 1 | ||
end | ||
ARGV.shift | ||
end | ||
end | ||
@options | ||
end | ||
|
||
private def subcommand | ||
return @subcommand if @subcommand | ||
options # Global options come before subcommands, so we have to look for them in ARGV first. | ||
@subcommand = ARGV[0] | ||
ARGV.shift | ||
@subcommand | ||
end | ||
|
||
private def each_input_file(&block) | ||
ARGF.each_file do |file| | ||
if ARGF.filename.end_with?(".md") || (ARGF.filename == "-" && markdown_option?) | ||
markdown_code_blocks(file).each do |code_block| | ||
block.call(code_block) | ||
end | ||
else | ||
input = file.read | ||
block.call(input.end_with?("\n") ? input : input << "\n") | ||
end | ||
end | ||
end | ||
|
||
private def markdown_code_blocks(file) | ||
markdown = Kramdown::Document.new(file.read) | ||
markdown.root.children.select{ |e| e.type == :codeblock && e.options[:lang] == "stone" }.map(&:value) | ||
end | ||
|
||
private def parse(input) | ||
parser.parse("#{input}\n", reporter: Parslet::ErrorReporter::Contextual.new) | ||
end | ||
|
||
private def transform(parse_tree) | ||
transformer = Stone::Transform.new | ||
ast = transformer.apply(parse_tree) | ||
ast.respond_to?(:compact) ? ast.compact : ast | ||
end | ||
|
||
private def markdown_option? | ||
options.include?("--markdown") | ||
end | ||
|
||
private def debug_option? | ||
options.include?("--debug") | ||
end | ||
|
||
private def top_context | ||
@top_context ||= Stone::Top.context | ||
Dry::CLI.new(self).call | ||
end | ||
|
||
end | ||
end | ||
|
||
|
||
# Load all the sub-commands. | ||
Dir[File.join(__dir__, "cli", "*.rb")].sort.each do |file| | ||
require file | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
require "dry/cli" | ||
require "kramdown" | ||
|
||
require "stone/language" | ||
|
||
|
||
module Stone | ||
module CLI | ||
|
||
class Command < Dry::CLI::Command | ||
|
||
private def each_input_file(files, markdown: false, &block) # rubocop:disable Metrics/MethodLength | ||
files.each do |filename| | ||
file = File.open(filename) # TODO: Handle `-` and handle missing files. | ||
if filename.end_with?(".md") || (filename == "-" && markdown) | ||
markdown_code_blocks(file).each do |code_block| | ||
block.call(code_block) | ||
end | ||
else | ||
input = file.read | ||
block.call(input.end_with?("\n") ? input : input << "\n") | ||
end | ||
end | ||
end | ||
|
||
private def language | ||
@language ||= Stone::Language::DEFAULT.new | ||
end | ||
|
||
private def markdown_code_blocks(file) | ||
markdown = Kramdown::Document.new(file.read) | ||
markdown.root.children.select{ |e| e.type == :codeblock && e.options[:lang] == "stone" }.map(&:value) | ||
end | ||
|
||
private def parse(input) | ||
parser.parse("#{input}\n", reporter: Parslet::ErrorReporter::Contextual.new) | ||
end | ||
|
||
private def transform(parse_tree) | ||
transformer = Stone::Transform.new | ||
ast = transformer.apply(parse_tree) | ||
ast.respond_to?(:compact) ? ast.compact : ast | ||
end | ||
|
||
end | ||
|
||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
require "parslet" | ||
|
||
require "stone/cli/command" | ||
require "stone/top" | ||
|
||
|
||
module Stone | ||
module CLI | ||
|
||
class Eval < Stone::CLI::Command | ||
|
||
desc "Output the result of each top-level expression (non-interactive REPL)" | ||
argument :source_files, type: :array, required: true, desc: "Source files" | ||
option :markdown, type: :boolean, default: false | ||
|
||
def call(source_files:, markdown:, **_args) | ||
each_input_file(source_files, markdown: markdown) do |input| | ||
top_context = Stone::Top.context | ||
puts language.ast(input).map{ |node| | ||
node.evaluate(top_context) | ||
}.compact | ||
rescue Parslet::ParseFailed => e | ||
puts e.parse_failure_cause.ascii_tree | ||
exit 1 | ||
end | ||
end | ||
|
||
end | ||
|
||
register "eval", Eval | ||
|
||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
require "parslet" | ||
|
||
require "stone/cli/command" | ||
|
||
|
||
module Stone | ||
module CLI | ||
|
||
class Parse < Stone::CLI::Command | ||
|
||
desc "Output the parse tree" | ||
argument :source_files, type: :array, required: true, desc: "Source files" | ||
option :markdown, type: :boolean, default: false | ||
|
||
def call(source_files:, markdown:, **_args) | ||
each_input_file(source_files, markdown: markdown) do |input| | ||
puts language.parse(input) | ||
rescue Parslet::ParseFailed => e | ||
puts e.parse_failure_cause.ascii_tree | ||
exit 1 | ||
end | ||
end | ||
|
||
end | ||
|
||
register "parse", Parse | ||
|
||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
require "readline" | ||
require "parslet" | ||
|
||
require "stone/cli/command" | ||
require "stone/top" | ||
|
||
|
||
module Stone | ||
module CLI | ||
|
||
class REPL < Stone::CLI::Command | ||
|
||
desc "Accept interactive manual input, and show the result of each top-level expression" | ||
|
||
def call(**_args) | ||
puts "Stone REPL" | ||
while (input = Readline.readline("#> ", true)) | ||
repl_1_line(input, top_context) | ||
end | ||
end | ||
|
||
private def repl_1_line(line, context) | ||
ast = language.ast(line, single_line: true) | ||
case result = ast.evaluate(context) | ||
when AST::Error | ||
puts "#! #{result}" | ||
when AST::Value | ||
puts "#= #{result}" | ||
end | ||
rescue Parslet::ParseFailed => e | ||
puts e.parse_failure_cause.ascii_tree | ||
end | ||
|
||
private def top_context | ||
@top_context ||= Stone::Top.context | ||
end | ||
|
||
end | ||
|
||
register "repl", REPL | ||
|
||
end | ||
end |
Oops, something went wrong.