├── bin └── .gitkeep ├── .travis.yml ├── src ├── zir │ ├── io.cr │ ├── logger.cr │ ├── macro.cr │ ├── command.cr │ ├── version.cr │ ├── parser.cr │ ├── bin │ │ └── bin.cr │ ├── engine.cr │ ├── io │ │ ├── reader.cr │ │ └── writer.cr │ ├── command │ │ └── executor.cr │ ├── macro │ │ └── macro.cr │ ├── logger │ │ └── logger.cr │ ├── parser │ │ └── parser.cr │ └── engine │ │ └── engine.cr └── zir.cr ├── spec ├── spec_helper.cr ├── zir_spec.cr ├── projs │ ├── p0 │ │ ├── zir.yaml │ │ └── test.c.z │ └── p1 │ │ ├── test0.c.z │ │ ├── Makefile │ │ ├── zir.yaml │ │ └── test1.c.z └── zir │ ├── command_spec.cr │ ├── parser_spec.cr │ └── engine_spec.cr ├── shard.yml ├── .gitignore ├── LICENSE └── README.md /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /src/zir/io.cr: -------------------------------------------------------------------------------- 1 | require "./io/*" 2 | -------------------------------------------------------------------------------- /src/zir/logger.cr: -------------------------------------------------------------------------------- 1 | require "./logger/*" 2 | -------------------------------------------------------------------------------- /src/zir/macro.cr: -------------------------------------------------------------------------------- 1 | require "./macro/*" 2 | -------------------------------------------------------------------------------- /src/zir/command.cr: -------------------------------------------------------------------------------- 1 | require "./command/*" 2 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/zir" 3 | -------------------------------------------------------------------------------- /src/zir/version.cr: -------------------------------------------------------------------------------- 1 | module Zir 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/zir_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Zir do 4 | end 5 | -------------------------------------------------------------------------------- /src/zir/parser.cr: -------------------------------------------------------------------------------- 1 | require "file_utils" 2 | require "yaml" 3 | require "./parser/*" 4 | -------------------------------------------------------------------------------- /src/zir/bin/bin.cr: -------------------------------------------------------------------------------- 1 | require "../zir" 2 | 3 | # Execute zir command 4 | cli = Zir::Cli.new 5 | cli.parse_option 6 | -------------------------------------------------------------------------------- /src/zir/engine.cr: -------------------------------------------------------------------------------- 1 | require "secure_random" 2 | require "./command" 3 | require "./parser" 4 | require "./engine/*" 5 | -------------------------------------------------------------------------------- /spec/projs/p0/zir.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | - test.c.z 3 | 4 | ids: 5 | rb0: ruby @file 6 | rb1: ruby @file 7 | 8 | finally: gcc test.c -o test 9 | -------------------------------------------------------------------------------- /spec/projs/p1/test0.c.z: -------------------------------------------------------------------------------- 1 | void hello(); 2 | 3 | int main(){ 4 | <-%ruby 10.times do |i| -> 5 | <-@ruby puts "hello();" -> 6 | <-%ruby end -> 7 | } 8 | -------------------------------------------------------------------------------- /spec/projs/p1/Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: test0.o test1.o 3 | gcc -o $@ $^ 4 | 5 | .c.o: 6 | gcc -c $< 7 | 8 | clean: 9 | rm -rf *.o *.c test .zir 10 | 11 | -------------------------------------------------------------------------------- /spec/projs/p1/zir.yaml: -------------------------------------------------------------------------------- 1 | 2 | targets: 3 | - test*.c.z 4 | 5 | ids: 6 | ruby: ruby @file 7 | python: python @file 8 | 9 | finally: 10 | make 11 | -------------------------------------------------------------------------------- /spec/projs/p1/test1.c.z: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void hello(){ 4 | <-%python import math -> 5 | <-@python print "printf(\"Hello zir! PI is %f\\n\");" % math.pi -> 6 | } 7 | -------------------------------------------------------------------------------- /spec/zir/command_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | include Zir::Executor 4 | 5 | describe Zir::Executor do 6 | 7 | it "cmd_exec" do 8 | cmd_exec("echo 0").should eq("0") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: zir 2 | version: 0.1.0 3 | 4 | authors: 5 | - taicsuzu 6 | 7 | crystal: 0.21.1 8 | 9 | license: MIT 10 | 11 | targets: 12 | zir: 13 | main: src/zir/bin/bin.cr 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /lib/ 3 | /bin/* 4 | /.shards/ 5 | 6 | # Libraries don't need dependency lock 7 | # Dependencies will be locked in application that uses them 8 | /shard.lock 9 | 10 | !.gitkeep 11 | .DS_Store 12 | 13 | .zir 14 | *.o 15 | test -------------------------------------------------------------------------------- /src/zir/io/reader.cr: -------------------------------------------------------------------------------- 1 | module Zir 2 | module Reader 3 | def read_target(target : String) : String 4 | begin 5 | File.read(target) 6 | rescue 7 | Logger.e "Failed to read a target: #{target}" 8 | exit 1 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/projs/p0/test.c.z: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(){ 4 | <-%rb0 10.times do |i| -> 5 | <-@rb0 puts "int v#{i} = #{i};" -> 6 | <-%rb0 end -> 7 | 8 | int res = 0; 9 | 10 | <-%rb1 10.times do |i| -> 11 | <-@rb1 puts "res += v#{i};" -> 12 | <-%rb1 end -> 13 | 14 | printf("The result is %d\n", res); 15 | return 0; 16 | } 17 | -------------------------------------------------------------------------------- /src/zir/command/executor.cr: -------------------------------------------------------------------------------- 1 | module Zir 2 | module Executor 3 | def cmd_exec(cmd : String) : String 4 | output = IO::Memory.new 5 | error = IO::Memory.new 6 | 7 | p = Process.run(cmd, shell: true, input: nil, output: output, error: error) 8 | 9 | if p.exit_status != 0 10 | Logger.e "Error is happen while expanding macros" 11 | Logger.e error.to_s 12 | exit 1 13 | end 14 | 15 | output.to_s.chomp 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/zir/io/writer.cr: -------------------------------------------------------------------------------- 1 | module Zir 2 | module Writer 3 | 4 | def write_target(target : String, code : String) : String 5 | filepath = zir_filepath(target) 6 | 7 | begin 8 | File.write(filepath, code) 9 | filepath 10 | rescue 11 | Logger.e "Failed to write a target into: #{filepath}" 12 | exit 1 13 | end 14 | end 15 | 16 | def zir_filepath(target : String) : String 17 | 18 | if m = /^(.*)\.(.*)\.z$/.match(target) 19 | return "#{m[1]}.#{m[2]}" 20 | else 21 | return "#{target}" 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/zir/macro/macro.cr: -------------------------------------------------------------------------------- 1 | module Zir 2 | # should be struct? 3 | class Macro 4 | getter idx : Int32 5 | getter tp : String 6 | getter id : String 7 | getter code : String 8 | getter mark : String 9 | 10 | property filepath : String? 11 | property result : String? 12 | 13 | def initialize(@idx : Int32, # index 14 | @tp : String, # type of the macro, @(Print) or %(Logic) 15 | @id : String, # identifier of the macro 16 | @code : String, # code itself 17 | @mark : String) # marking string 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/zir/logger/logger.cr: -------------------------------------------------------------------------------- 1 | require "logger" 2 | 3 | module Zir 4 | class Logger 5 | @@logger : Logger? 6 | @@quiet : Bool = false 7 | 8 | def initialize 9 | end 10 | 11 | def self.set_quiet(@@quiet : Bool) 12 | end 13 | 14 | def self.i(msg) 15 | @@logger = Logger.new if @@logger.nil? 16 | @@logger.not_nil!.i(msg) 17 | end 18 | 19 | def i(msg) 20 | puts "\e[36m[zir]\e[m #{msg}" unless @@quiet 21 | end 22 | 23 | def self.w(msg) 24 | @@logger = Logger.new if @@logger.nil? 25 | @@logger.not_nil!.w(msg) 26 | end 27 | 28 | def w(msg) 29 | puts "\e[33m[zir]\e[m #{msg}" unless @@quiet 30 | end 31 | 32 | def self.e(msg) 33 | @@logger = Logger.new if @@logger.nil? 34 | @@logger.not_nil!.e(msg) 35 | end 36 | 37 | def e(msg) 38 | puts "\e[31m[zir]\e[m #{msg}" unless @@quiet 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Taichiro Suzuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/zir/parser/parser.cr: -------------------------------------------------------------------------------- 1 | module Zir 2 | # Parsing zir.yaml and serving information from it 3 | module Parser 4 | TARGETS = "targets" 5 | IDS = "ids" 6 | FINALLY = "finally" 7 | 8 | @yaml : YAML::Any? 9 | 10 | def yaml_path : String 11 | "#{Dir.current}/zir.yaml" 12 | end 13 | 14 | # Read zir.yaml from current directory 15 | def yaml : YAML::Any 16 | @yaml = YAML.parse(File.read(yaml_path)) if @yaml.nil? 17 | @yaml.not_nil! 18 | end 19 | 20 | # Parse zir.yaml and return command 21 | def get_cmd(id : String) : String 22 | begin 23 | yaml[IDS][id].to_s 24 | rescue 25 | Logger.e "Failed to find command for id in #{yaml_path}: #{id}" 26 | exit 1 27 | end 28 | end 29 | 30 | # Returned targets are absolute file pathes 31 | def get_targets : Array(String) 32 | begin 33 | res = Array(String).new 34 | yaml[TARGETS].each do |t| 35 | # Adding all matched pathes by `Dir.glob` but directories or not .z files are removed from them 36 | # Pathes are converted to absolute path 37 | res.concat(Dir.glob(File.expand_path(t.to_s)).reject{ |path| File.directory?(path) || !path.match(/^.*\.z$/) }) 38 | end 39 | res 40 | rescue 41 | Logger.e "Failed to find targets in #{yaml_path}" 42 | exit 1 43 | end 44 | end 45 | 46 | def get_finally : String 47 | begin 48 | yaml[FINALLY].to_s 49 | rescue 50 | Logger.e "Failed to find a `finally` command in #{yaml_path}" 51 | exit 1 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/zir/parser_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | require "yaml" 3 | 4 | class ParserForSpec 5 | include Zir::Parser 6 | end 7 | 8 | source_yaml_p0 = <<-SOURCE_YAML_P0 9 | targets: 10 | - test.c.z 11 | 12 | ids: 13 | rb0: ruby @file 14 | rb1: ruby @file 15 | 16 | finally: gcc test.c -o test 17 | SOURCE_YAML_P0 18 | 19 | source_yaml_p1 = <<-SOURCE_YAML_P1 20 | targets: 21 | - test*.c.z 22 | 23 | ids: 24 | ruby: ruby @file 25 | python: python @file 26 | 27 | finally: 28 | make 29 | SOURCE_YAML_P1 30 | 31 | Dir.cd "spec/projs/p0" do 32 | 33 | describe Zir::Parser do 34 | 35 | it "yaml_path" do 36 | parser = ParserForSpec.new 37 | parser.yaml_path.should eq("#{Dir.current}/zir.yaml") 38 | end 39 | 40 | it "yaml" do 41 | parser = ParserForSpec.new 42 | y0 = YAML.parse(source_yaml_p0) 43 | y1 = parser.yaml 44 | y0.should eq(y1) 45 | end 46 | 47 | it "get_cmd" do 48 | parser = ParserForSpec.new 49 | parser.get_cmd("rb0").should eq("ruby @file") 50 | parser.get_cmd("rb1").should eq("ruby @file") 51 | end 52 | 53 | it "get_targets" do 54 | parser = ParserForSpec.new 55 | targets = parser.get_targets 56 | targets.size.should eq(1) 57 | targets[0].should eq(File.expand_path("test.c.z")) 58 | end 59 | 60 | it "get_finally" do 61 | parser = ParserForSpec.new 62 | parser.get_finally.should eq("gcc test.c -o test") 63 | end 64 | end 65 | end 66 | 67 | Dir.cd "spec/projs/p1" do 68 | 69 | describe Zir::Parser do 70 | 71 | it "yaml_path" do 72 | parser = ParserForSpec.new 73 | parser.yaml_path.should eq("#{Dir.current}/zir.yaml") 74 | end 75 | 76 | it "yaml" do 77 | parser = ParserForSpec.new 78 | 79 | y0 = YAML.parse(source_yaml_p1) 80 | y1 = parser.yaml 81 | y0.should eq(y1) 82 | end 83 | 84 | it "get_cmd" do 85 | parser = ParserForSpec.new 86 | parser.get_cmd("ruby").should eq("ruby @file") 87 | parser.get_cmd("python").should eq("python @file") 88 | end 89 | 90 | it "get_targets" do 91 | parser = ParserForSpec.new 92 | targets = parser.get_targets 93 | targets.size.should eq(2) 94 | targets[0].should eq(File.expand_path("test0.c.z")) 95 | targets[1].should eq(File.expand_path("test1.c.z")) 96 | end 97 | 98 | it "get_finally" do 99 | parser = ParserForSpec.new 100 | parser.get_finally.should eq("make") 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/zir/engine_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | Dir.cd "spec/projs/p0" do 4 | describe Zir::Engine do 5 | 6 | code0 = <<-CODE0 7 | <-%rb0 a = 4 -> 8 | <-%rb0 a.times do |a| -> 9 | <-@rb0 puts "int a\#{a} = \#{a};"-> 10 | <-%rb0 end -> 11 | CODE0 12 | 13 | code1 = <<-CODE1 14 | <-%rb0 a = 100 -> 15 | <-%rb1 b = 101 -> 16 | <-@rb0 puts "int a = \#{a};" -> 17 | <-@rb1 puts "int b = \#{b};" -> 18 | CODE1 19 | 20 | it "#scan_code code0" do 21 | engine = Zir::Engine.new(code0) 22 | engine.scan_code 23 | 24 | hex = engine.hex 25 | 26 | engine.macros.each_with_index do |m, i| 27 | m.idx.should eq(i) 28 | m.id.should eq("rb0") 29 | m.mark.should eq("__#{hex}_#{m.idx}__") 30 | end 31 | end 32 | 33 | it "#scan_code code1" do 34 | engine = Zir::Engine.new(code1) 35 | engine.scan_code 36 | 37 | hex = engine.hex 38 | 39 | engine.macros.each_with_index do |m, i| 40 | m.idx.should eq(i) 41 | m.id.should eq(i%2 == 0 ? "rb0" : "rb1") 42 | m.mark.should eq("__#{hex}_#{m.idx}__") 43 | end 44 | end 45 | 46 | it "#scan_id_group code0" do 47 | engine = Zir::Engine.new(code0) 48 | engine.scan_code 49 | engine.scan_id_group 50 | engine.clean 51 | 52 | engine.macros[0].filepath.should eq(nil) 53 | engine.macros[1].filepath.should eq(nil) 54 | engine.macros[2].filepath.should eq("#{Dir.current}/.zir/__#{engine.hex}_2__") 55 | engine.macros[3].filepath.should eq(nil) 56 | end 57 | 58 | it "#scan_id_group code1" do 59 | engine = Zir::Engine.new(code1) 60 | engine.scan_code 61 | engine.scan_id_group 62 | engine.clean 63 | 64 | engine.macros[0].filepath.should eq(nil) 65 | engine.macros[1].filepath.should eq(nil) 66 | engine.macros[2].filepath.should eq("#{Dir.current}/.zir/__#{engine.hex}_2__") 67 | engine.macros[3].filepath.should eq("#{Dir.current}/.zir/__#{engine.hex}_3__") 68 | end 69 | 70 | it "exec_macro code0" do 71 | engine = Zir::Engine.new(code0) 72 | engine.scan_code 73 | engine.scan_id_group 74 | engine.exec_macro 75 | engine.clean 76 | 77 | engine.macros[0].result.should eq(nil) 78 | engine.macros[1].result.should eq(nil) 79 | engine.macros[2].result.should eq("int a0 = 0;\nint a1 = 1;\nint a2 = 2;\nint a3 = 3;") 80 | engine.macros[3].result.should eq(nil) 81 | end 82 | 83 | it "exec_macro code1" do 84 | engine = Zir::Engine.new(code1) 85 | engine.scan_code 86 | engine.scan_id_group 87 | engine.exec_macro 88 | engine.clean 89 | 90 | engine.macros[0].result.should eq(nil) 91 | engine.macros[1].result.should eq(nil) 92 | engine.macros[2].result.should eq("int a = 100;") 93 | engine.macros[3].result.should eq("int b = 101;") 94 | end 95 | 96 | it "#run code0" do 97 | engine = Zir::Engine.new(code0) 98 | engine.run 99 | engine.clean 100 | engine.code.should eq("\n\nint a0 = 0;\nint a1 = 1;\nint a2 = 2;\nint a3 = 3;\n") 101 | end 102 | 103 | it "#run code1" do 104 | engine = Zir::Engine.new(code1) 105 | engine.run 106 | engine.clean 107 | engine.code.should eq("\n\nint a = 100;\nint b = 101;") 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /src/zir.cr: -------------------------------------------------------------------------------- 1 | require "./zir/*" 2 | require "file_utils" 3 | require "option_parser" 4 | 5 | module Zir 6 | 7 | # Client for zir command 8 | class Cli 9 | @clean_depth : Int32 = 1 10 | 11 | # Parsing options for zir command 12 | def parse_option 13 | option_parser = OptionParser.new do |parser| 14 | 15 | parser.banner = "Usage: zir [arguments] [run, init, clean]" 16 | 17 | parser.on "-v", "--version", "Show the version" do 18 | Logger.i "version: #{Zir::VERSION}" 19 | exit 20 | end 21 | 22 | parser.on "-q", "--quiet", "Disable all logs (Default is not quiet)" do 23 | Logger.set_quiet(true) 24 | end 25 | 26 | parser.on "-h", "--help", "Show the helps" do 27 | puts parser 28 | puts <<-CMD 29 | 30 | run Execute zir command 31 | init Create sample zir.yaml into current directory 32 | clean Clean all temporary files created by zir 33 | CMD 34 | exit 35 | end 36 | 37 | # 0: Cleaning nothing, keep every temporary files that zir created. 38 | # 1: Clean temp files to execute macro. 39 | # 2: Clean every files created by zir 40 | parser.on "-c DEPTH", "--clean=DEPTH", "Set the depth of the cleaning. DEPTH can be 0 to 2. Default is 1." do |depth| 41 | if depth != "0" && depth != "1" && depth != "2" 42 | Logger.w "Invalid depth: \"#{depth}\", depth can be 0 to 2 (clean depth will be 2)" 43 | else 44 | @clean_depth = depth.to_i 45 | end 46 | end 47 | 48 | parser.unknown_args do |args| 49 | args.each do |arg| 50 | case arg 51 | when "run" 52 | main 53 | when "init" 54 | init 55 | when "clean" 56 | clean 57 | end 58 | end 59 | end 60 | end 61 | 62 | begin 63 | option_parser.parse(ARGV.clone) 64 | rescue e 65 | Logger.e "Oops..." 66 | if message = e.message 67 | Logger.e message 68 | end 69 | end 70 | end 71 | 72 | # Create sample zir.yaml into a current library 73 | def init 74 | # Checking an existance of zir.yaml in the current directory 75 | if File.exists?("zir.yaml") 76 | Logger.w "zir.yaml already eixsts in the current directory" 77 | else 78 | File.write "zir.yaml", <<-SAMPLE 79 | targets: # Targets to be expandedd by zir 80 | - src/main.c 81 | - src/libs/*.c 82 | 83 | ids: # identifiers for each script 84 | ruby: ruby @file 85 | python: python @file 86 | 87 | finally: # command to be executed at last 88 | gcc -o main main.c.z 89 | SAMPLE 90 | end 91 | end 92 | 93 | # Main function 94 | def main 95 | Logger.i "Starting zir [#{Zir::VERSION}]" 96 | 97 | # file pathes that zir created 98 | zir_files = Array(String).new 99 | 100 | # Get codes which include zir macro 101 | targets = get_targets 102 | targets.each do |target| 103 | 104 | # Code to be expanded by zir 105 | code = read_target(target) 106 | 107 | # Initialize and running zir engine 108 | engine = Zir::Engine.new(code) 109 | engine.run 110 | engine.clean if @clean_depth >= 1 111 | 112 | # Dump expanded code into zir file 113 | zir_file = write_target(target, engine.code) 114 | 115 | # Add the file to the list 116 | zir_files.push(zir_file) 117 | 118 | Logger.i "zir file create at #{zir_file}" 119 | end 120 | 121 | Logger.i "#{zir_files.size} files are expanded in total" 122 | 123 | # Execute finally command 124 | cmd_exec(get_finally) 125 | 126 | # clean: depth is 2 127 | zir_files.each do |zir_file| 128 | Logger.i "Delete #{zir_file}" 129 | FileUtils.rm(zir_file) 130 | end if @clean_depth == 2 131 | 132 | clean if @clean_depth >= 1 133 | end 134 | 135 | def clean 136 | Logger.i "Delete .zir directory" 137 | FileUtils.rm_rf("#{Dir.current}/.zir") if File.directory?("#{Dir.current}/.zir") 138 | end 139 | 140 | include Parser 141 | include Reader 142 | include Writer 143 | include Executor 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zir 2 | 3 | [![Build Status](https://travis-ci.org/tbrand/zir.svg?branch=master)](https://travis-ci.org/tbrand/zir) 4 | [![Dependency Status](https://shards.rocks/badge/github/tbrand/zir/status.svg)](https://shards.rocks/github/tbrand/zir) 5 | [![devDependency Status](https://shards.rocks/badge/github/tbrand/zir/dev_status.svg)](https://shards.rocks/github/tbrand/zir) 6 | 7 | **zir** is a command line tool that realizes to write macros in any scripts into any languages. 8 | See an example. (The macro is written in Ruby) 9 | ```c 10 | #include 11 | 12 | int main(){ 13 | <-@macro puts "double a = #{Math::PI};" -> 14 | printf("PI is %f!\n", a); 15 | return 0; 16 | } 17 | ``` 18 | This file will be expanded by zir like 19 | ```c 20 | #include 21 | 22 | int main(){ 23 | double a = 3.141592653589793; 24 | printf("PI is %f!\n", a); 25 | return 0; 26 | } 27 | ``` 28 | 29 | The result is `PI is 3.141593!`. :smile: 30 | You can find other samples at [here](https://github.com/tbrand/zir/tree/master/spec/projs). 31 | 32 | ## Installation 33 | 34 | zir is written in Crystal. So you need the environment. After that, clone this project and build by 35 | ``` 36 | shards build 37 | ``` 38 | Now you can find an executable binary at `zir/bin/zir` 39 | 40 | ## Usage 41 | 42 | ### Flow of the expandation 43 | 44 | 1. Search zir files (such as sample.c.z) 45 | 2. Collect macros from the files 46 | 3. Create temporary files to be executed 47 | 4. Execute the scripts 48 | 5. Embed the result into the files (such as sample.c) 49 | 50 | ### zir.yaml 51 | 52 | zir.yaml is a configuration file to execute zir. You must put it on a root of your project. zir.yaml is consists of 3 parts. 53 | 54 | Specify files to be expanded in **targets**. The files must end with **.z**. **.z** will be removed from the name of expanded files. So sample.c.z will be sample.c. 55 | ```yaml 56 | targets: # An example 57 | - sample.c.z 58 | ``` 59 | 60 | Tell me how to execute the macros in **ids**. It need identifier and command line sample. **@file** will be replaced to a temporary executable. 61 | ```yaml 62 | # 'macro' is an identifier and `ruby some_temporary_executable` will be executed 63 | ids: 64 | macro: ruby @file 65 | ``` 66 | 67 | What to execute at finally? 68 | ```yaml 69 | finally: 70 | gcc -o a.out sample.c 71 | ``` 72 | 73 | Here is a fully example. 74 | ```yaml 75 | targets: 76 | - sample.c.z 77 | 78 | ids: 79 | macro: ruby @file 80 | 81 | finally: 82 | gcc -o a.out sample.c 83 | ``` 84 | 85 | ### Write macros 86 | 87 | The structure of macros is here. 88 | ``` 89 | <-@id your_code_here -> 90 | ``` 91 | 92 | All macros are sandwiched by `<- ->`. In the above example, `@` is called **mark** and it can be `%` as well. `id` is an identifier which is defined in zir.yaml. Puts your code at `your_code_here`. 93 | 94 | There are 2 types of macros and each of them has their mark. 95 | 96 | First one is **print macro** that will be embeded into a source code. The mark of the print macro is **@**. Print macro shouldn't contain any logics but just print out variables. Here is an example of it. 97 | ``` 98 | <-@id puts "a" -> 99 | ``` 100 | 101 | Second one is **logic macro** that contains logics only. So This will print out nothing. Logic macros affect to the print macros which have same id with it. 102 | ``` 103 | <-%id0 a = 10 -> 104 | <-%id1 a += 1 -> 105 | <-@id0 puts a -> 106 | ``` 107 | So the result of this will be 10. 108 | See [sample projects](https://github.com/tbrand/zir/tree/master/spec/projs) to know how to write these macros. 109 | 110 | ### command line 111 | 112 | Basically, you just run `zir run` at root of your project. If you want to clean all temporary files, you can do it by `zir clean`. If you need a sample of zir.yaml, you can get it by `zir init`. 113 | 114 | `-c DEPTH` or `--cealn=DEPTH` options help you to debug. You can specify which files to keep or to delete. DEPTH can be 0 to 2. If you specify 0, zir will keep all temporary files. Intermediate executable scripts are in .zir directory. 1 is default value that delete .zir directory but keep expanded files. So when you expand sample.c.z, expanded sample.c will remain. 2 will delete all files created by zir. (Delete sample.c in the previous case.) 115 | 116 | `zir -h` will show you more options. 117 | 118 | ## Contributing 119 | 120 | 1. Fork it ( https://github.com/tbrand/zir/fork ) 121 | 2. Create your feature branch (git checkout -b my-new-feature) 122 | 3. Commit your changes (git commit -am 'Add some feature') 123 | 4. Push to the branch (git push origin my-new-feature) 124 | 5. Create a new Pull Request 125 | 126 | ## Contributors 127 | 128 | - [tbrand](https://github.com/tbrand) Taichiro Suzuki - creator, maintainer 129 | -------------------------------------------------------------------------------- /src/zir/engine/engine.cr: -------------------------------------------------------------------------------- 1 | module Zir 2 | class Engine 3 | # Template of macro 4 | # For example, this will match with 5 | # ``` 6 | # <-%ruby a = a + 1 -> 7 | # <-@ruby print a -> 8 | # ``` 9 | M_TEMPLATE = /<-(@|%)(.*?)[\s|\n]([\s|\S]*?)->/ 10 | 11 | # Collected macros 12 | getter macros : Array(Macro) 13 | getter code : String 14 | getter hex : String 15 | 16 | def initialize(@code : String) 17 | @macros = Array(Macro).new 18 | @hex = SecureRandom.hex 19 | end 20 | 21 | def run 22 | # Scan the code to 23 | # 1. Collect macros 24 | # 2. Replace the macros to a unique mark 25 | scan_code 26 | 27 | # Scanning and grouping each ids 28 | # id is `abc` of <-%abc code here -> 29 | scan_id_group 30 | 31 | # Execute macros and set result 32 | exec_macro 33 | 34 | # Expand macros into code 35 | expand_macro 36 | end 37 | 38 | def scan_code 39 | # Index of each macro 40 | idx = 0 41 | 42 | # Replace each macro to a unique mark 43 | @code = @code.gsub(M_TEMPLATE) do |m| 44 | 45 | # A unique mark 46 | mark = "__#{@hex}_#{idx}__" 47 | 48 | # Scanning the macro again to create a Macro object 49 | m.scan(M_TEMPLATE) do |match| 50 | 51 | tp = match[1] # type of the macro 52 | id = match[2] # identifier of the macro 53 | code = match[3] # code itself 54 | 55 | # Pushing the object to the array 56 | @macros.push(Macro.new(idx, tp, id, code, mark)) 57 | end 58 | 59 | idx += 1 60 | 61 | # Replace the match to the mark 62 | mark 63 | end 64 | end 65 | 66 | # Scan id groups to create executable files 67 | def scan_id_group 68 | 69 | # Grouping macros with their id(such as `crystal` or `ruby`) 70 | # The id can be an arbitrary value 71 | grouped_macros = @macros.group_by{ |m| m.id } 72 | 73 | # For each group, create a temporary file to be executed 74 | grouped_macros.each_key do |id| 75 | 76 | # Pick one of the group 77 | id_group = grouped_macros[id] 78 | 79 | # Grouping the macros with their types(@ or %) 80 | tp_group = id_group.group_by{ |m| m.tp } 81 | 82 | # If the group doesn't contain a macro of type of `@`, 83 | # it means that the macros in the group print out nothing 84 | # So zir ignores them 85 | return unless tp_group.has_key?("@") 86 | 87 | # For each macro of type of `@`, create a temporary file to be executed 88 | # All logic types of macros(type of `%`) will be added into the file to keep a logic concurrency 89 | tp_group["@"].each do |tp_print| 90 | 91 | # Create an executable and set it to the print macro 92 | tp_print.filepath = create_executable(tp_print, tp_group) 93 | end 94 | end 95 | end 96 | 97 | # Create executables for `@` type macros 98 | # All logic macros will be added into the executables to keep a concurrency 99 | def create_executable(tp_print, tp_group) : String 100 | 101 | # The file name 102 | filepath = "#{tmp_for_executable}/#{tp_print.mark}" 103 | 104 | File.open(filepath, "w") do |file| 105 | 106 | printed = false 107 | 108 | # For each logic macros 109 | tp_group["%"].each do |tp_logic| 110 | 111 | # Insert the print macro if it's in between some logics 112 | if tp_logic.idx > tp_print.idx 113 | file.puts tp_print.code 114 | printed = true 115 | end 116 | 117 | file.puts tp_logic.code 118 | 119 | end if tp_group.has_key?("%") # The macros in the group has logics 120 | 121 | file.puts tp_print.code unless printed 122 | end 123 | 124 | filepath 125 | end 126 | 127 | # Execute macros by using temp executables 128 | # Set the result to Macro object 129 | def exec_macro 130 | @macros.each do |m| 131 | 132 | if filepath = m.filepath 133 | 134 | # Execute a temp file and get result 135 | if cmd = get_cmd(m.id) 136 | # todo: check an existance of @file 137 | if cmd.includes?("@file") 138 | m.result = cmd_exec(cmd.gsub("@file", filepath)) 139 | else 140 | Logger.e "The command line of '#{m.id}' doesn't include '@file'." 141 | Logger.e "Please tell me how to execute temp executables." 142 | end 143 | end 144 | end if m.tp == "@" 145 | end 146 | end 147 | 148 | # Expand macros into the code 149 | def expand_macro 150 | @macros.each do |m| 151 | @code = @code.sub(m.mark, m.result) 152 | end 153 | end 154 | 155 | def clean 156 | @macros.each do |m| 157 | if filepath = m.filepath 158 | # Delete the temp file 159 | FileUtils.rm(filepath) 160 | end if m.tp == "@" 161 | end 162 | end 163 | 164 | def tmp_for_executable : String 165 | path = "#{Dir.current}/.zir" 166 | Dir.mkdir(path) unless Dir.exists?(path) 167 | path 168 | end 169 | 170 | include Parser 171 | include Executor 172 | end 173 | end 174 | --------------------------------------------------------------------------------