Skip to content

Commit

Permalink
Implement "crystal tool expand" command
Browse files Browse the repository at this point in the history
"crystal tool expand" expands macros for given location.

https://asciinema.org/a/96590
  • Loading branch information
makenowjust committed Jan 12, 2017
1 parent 591f8ef commit 14d2eae
Show file tree
Hide file tree
Showing 8 changed files with 711 additions and 19 deletions.
474 changes: 474 additions & 0 deletions spec/compiler/crystal/tools/expand_spec.cr

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion src/compiler/crystal/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Crystal::Command
Tool:
context show context for given location
expand show macro expansion for given location
format format project, directories and/or files
hierarchy show type hierarchy
implementations show implementations for given call in location
Expand Down Expand Up @@ -141,6 +142,9 @@ class Crystal::Command
when "format".starts_with?(tool)
options.shift
format
when "expand".starts_with?(tool)
options.shift
expand
when "hierarchy".starts_with?(tool)
options.shift
hierarchy
Expand Down Expand Up @@ -198,9 +202,10 @@ class Crystal::Command
end
end

private def compile_no_codegen(command, wants_doc = false, hierarchy = false, cursor_command = false, top_level = false)
private def compile_no_codegen(command, wants_doc = false, hierarchy = false, no_cleanup = false, cursor_command = false, top_level = false)
config = create_compiler command, no_codegen: true, hierarchy: hierarchy, cursor_command: cursor_command
config.compiler.no_codegen = true
config.compiler.no_cleanup = no_cleanup
config.compiler.wants_doc = wants_doc
result = top_level ? config.top_level_semantic : config.compile
{config, result}
Expand Down
10 changes: 8 additions & 2 deletions src/compiler/crystal/command/cursor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ class Crystal::Command
end
end

private def cursor_command(command)
config, result = compile_no_codegen command, cursor_command: true
private def expand
cursor_command("tool expand", no_cleanup: true, wants_doc: true) do |location, config, result|
result = ExpandVisitor.new(location).process(result)
end
end

private def cursor_command(command, no_cleanup = false, wants_doc = false)
config, result = compile_no_codegen command, cursor_command: true, no_cleanup: no_cleanup, wants_doc: wants_doc

format = config.output_format

Expand Down
5 changes: 4 additions & 1 deletion src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ module Crystal
# If `false`, color won't be used in output messages.
property? color = true

# If `true`, skip cleanup process on semantic analysis.
property? no_cleanup = false

# If `true`, no executable will be generated after compilation
# (useful to type-check a prorgam)
property? no_codegen = false
Expand Down Expand Up @@ -131,7 +134,7 @@ module Crystal
source = [source] unless source.is_a?(Array)
program = new_program(source)
node = parse program, source
node = program.semantic node, @stats
node = program.semantic node, @stats, cleanup: !no_cleanup?
codegen program, node, source, output_filename unless @no_codegen
Result.new program, node
end
Expand Down
7 changes: 5 additions & 2 deletions src/compiler/crystal/semantic.cr
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Crystal::Program
# Runs semantic analysis on the given node, returning a node
# that's typed. In the process types and methods are defined in
# this program.
def semantic(node : ASTNode, stats = false) : ASTNode
def semantic(node : ASTNode, stats = false, cleanup = true) : ASTNode
node, processor = top_level_semantic(node, stats: stats)

Crystal.timing("Semantic (cvars initializers)", stats) do
Expand All @@ -35,15 +35,18 @@ class Crystal::Program
end

result = Crystal.timing("Semantic (main)", stats) do
visit_main(node, process_finished_hooks: true)
visit_main(node, process_finished_hooks: true, cleanup: cleanup)
end

Crystal.timing("Semantic (cleanup)", stats) do
cleanup_types
cleanup_files
end

Crystal.timing("Semantic (recursive struct check)", stats) do
RecursiveStructChecker.new(self).run
end

result
end

Expand Down
4 changes: 2 additions & 2 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ require "./semantic_visitor"

module Crystal
class Program
def visit_main(node, visitor = MainVisitor.new(self), process_finished_hooks = false)
def visit_main(node, visitor = MainVisitor.new(self), process_finished_hooks = false, cleanup = true)
node.accept visitor
program.process_finished_hooks(visitor) if process_finished_hooks

missing_types = FixMissingTypes.new(self)
node.accept missing_types
program.process_finished_hooks(missing_types) if process_finished_hooks

node = cleanup node
node = cleanup node if cleanup

if process_finished_hooks
finished_hooks.map! do |hook|
Expand Down
33 changes: 22 additions & 11 deletions src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2792,10 +2792,12 @@ module Crystal
def parse_percent_macro_expression
raise "can't nest macro expressions", @token if @in_macro_expression

location = @token.location
macro_exp = parse_macro_expression
check_macro_expression_end
end_location = token_end_location
next_token
MacroExpression.new(macro_exp)
MacroExpression.new(macro_exp).at(location).at_end(end_location)
end

def parse_macro_expression
Expand Down Expand Up @@ -2870,7 +2872,7 @@ module Crystal
next_token_skip_space
check :"%}"

return MacroFor.new(vars, exp, body)
return MacroFor.new(vars, exp, body).at_end(token_end_location)
when :if
return parse_macro_if(start_line, start_column, macro_state)
when :unless
Expand All @@ -2894,7 +2896,7 @@ module Crystal
next_token_skip_space
check :"%}"

return MacroIf.new(BoolLiteral.new(true), body)
return MacroIf.new(BoolLiteral.new(true), body).at_end(token_end_location)
when :else, :elsif, :end
return nil
end
Expand All @@ -2904,7 +2906,7 @@ module Crystal
exps = parse_expressions
@in_macro_expression = false

MacroExpression.new(exps, output: false)
MacroExpression.new(exps, output: false).at_end(token_end_location)
end

def parse_macro_if(start_line, start_column, macro_state, check_end = true)
Expand All @@ -2916,7 +2918,7 @@ module Crystal

if @token.type != :"%}" && check_end
an_if = parse_if_after_condition cond, true
return MacroExpression.new(an_if, output: false)
return MacroExpression.new(an_if, output: false).at_end(token_end_location)
end

check :"%}"
Expand Down Expand Up @@ -2956,7 +2958,7 @@ module Crystal
unexpected_token
end

return MacroIf.new(cond, a_then, a_else)
return MacroIf.new(cond, a_then, a_else).at_end(token_end_location)
end

def parse_expression_inside_macro
Expand Down Expand Up @@ -3664,7 +3666,7 @@ module Crystal
end
end
node.doc = doc
node.location = block.try(&.location) || location
node.location = location
node.end_location = block.try(&.end_location) || call_args.try(&.end_location) || end_location
node
end
Expand Down Expand Up @@ -4738,6 +4740,7 @@ module Crystal
end

def parse_lib
location = @token.location
next_token_skip_space_or_newline

name = check_const
Expand All @@ -4747,9 +4750,10 @@ module Crystal
body = push_visbility { parse_lib_body_expressions }

check_ident :end
end_location = token_end_location
next_token_skip_space

LibDef.new name, body, name_column_number
LibDef.new(name, body, name_column_number).at(location).at_end(end_location)
end

def parse_lib_body
Expand Down Expand Up @@ -4836,6 +4840,7 @@ module Crystal
IdentOrConst = [:IDENT, :CONST]

def parse_fun_def(require_body = false)
location = @token.location
doc = @token.doc

push_def if require_body
Expand Down Expand Up @@ -4914,20 +4919,22 @@ module Crystal
if require_body
if @token.keyword?(:end)
body = Nop.new
end_location = token_end_location
next_token
else
body = parse_expressions
body, end_location = parse_exception_handler body, implicit: true
end
else
body = nil
end_location = token_end_location
end

pop_def if require_body

fun_def = FunDef.new name, args, return_type, varargs, body, real_name
fun_def.doc = doc
fun_def
fun_def.at(location).at_end(end_location)
end

def parse_alias
Expand Down Expand Up @@ -5008,14 +5015,16 @@ module Crystal
end

def parse_c_struct_or_union(union : Bool)
location = @token.location
next_token_skip_space_or_newline
name = check_const
next_token_skip_statement_end
body = parse_c_struct_or_union_body_expressions
check_ident :end
end_location = token_end_location
next_token_skip_space

CStructOrUnionDef.new name, Expressions.from(body), union: union
CStructOrUnionDef.new(name, Expressions.from(body), union: union).at(location).at_end(end_location)
end

def parse_c_struct_or_union_body
Expand Down Expand Up @@ -5082,6 +5091,7 @@ module Crystal
end

def parse_enum_def
location = @token.location
doc = @token.doc

next_token_skip_space_or_newline
Expand All @@ -5103,13 +5113,14 @@ module Crystal
members = parse_enum_body_expressions

check_ident :end
end_location = token_end_location
next_token_skip_space

raise "Bug: EnumDef name can only be a Path" unless name.is_a?(Path)

enum_def = EnumDef.new name, members, base_type
enum_def.doc = doc
enum_def
enum_def.at(location).at_end(end_location)
end

def parse_enum_body
Expand Down
Loading

0 comments on commit 14d2eae

Please sign in to comment.