smacro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2f5c8cb7f5f7c4c6deec9cf8f0d023d4bcdc152e8cf7378c2b624fd7a5c473eb
4
+ data.tar.gz: 1120a714fab2e01ee6e682e972e6e3d86e1b62c8c461311f6805adc91af627d3
5
+ SHA512:
6
+ metadata.gz: 4737d41ed76a54a289fd41a8490b2caea1fe502664b29e7d7a0ac24dbe32b960437dd3a412e92dfca477f4b4cce11c96ea2ed202e0717780c39f44fef8788157
7
+ data.tar.gz: d07b412b1968f1fc6b2b7a68458b1494b1bab273964551b0fd5da4878019a3d530a3143c13132604fc04daeabac7a20337882b44968a625209c9c42dc5a03858
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # smacro
2
+
3
+ `smacro` expands Ruby macros before handing the resulting source to the Spinel AOT compiler. Macro definitions run under CRuby at build time; the generated program does not need the gem at runtime.
4
+
5
+ ## Install locally
6
+
7
+ ```sh
8
+ gem build smacro.gemspec
9
+ gem install ./smacro-0.1.0.gem
10
+ ```
11
+
12
+ The gem requires Ruby 3.0 or later and Prism. Spinel is needed to compile the expanded result.
13
+
14
+ ## Use
15
+
16
+ Put macro definitions in `app.smacro.rb` next to `app.rb`:
17
+
18
+ ```ruby
19
+ Smacro.define(:fields) do |*names|
20
+ "attr_accessor " + names.map { |name| ":#{name}" }.join(", ")
21
+ end
22
+ ```
23
+
24
+ Call the macro in `app.rb`:
25
+
26
+ ```ruby
27
+ class User
28
+ fields :name, :email
29
+ end
30
+ ```
31
+
32
+ Expand and compile:
33
+
34
+ ```sh
35
+ smacro app.rb -o app.expanded.rb
36
+ spinel app.expanded.rb
37
+ ```
38
+
39
+ Pass a macro file as the second argument when its name differs from `app.smacro.rb`. The existing `ruby smacro.rb app.rb` command still works. For Ruby code, use `require "smacro"` to access `Smacro.define` and `Smacro.expand`.
40
+
41
+ The current prototype accepts literal macro arguments (symbols, strings, integers, booleans, nil, and arrays). It does not yet support macro definitions inside the source file, nested macro calls, or Crystal's AST template syntax. `sh run.sh` compares the sample's CRuby and Spinel results.
data/exe/smacro ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require "smacro/cli"
3
+
4
+ Smacro::CLI.run(ARGV)
data/lib/smacro/cli.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "optparse"
2
+ require_relative "../smacro"
3
+
4
+ module Smacro
5
+ module CLI
6
+ def self.run(argv)
7
+ options = {}
8
+ parser = OptionParser.new do |opts|
9
+ opts.banner = "Usage: smacro [options] app.rb [app.smacro.rb]"
10
+ opts.on("-o", "--output FILE", "Write expanded Ruby to FILE") { |path| options[:output] = path }
11
+ opts.on("-v", "--version", "Print version") { puts Smacro::VERSION; return }
12
+ opts.on("-h", "--help", "Show help") { puts opts; return }
13
+ end
14
+ parser.parse!(argv)
15
+ abort parser.to_s unless (1..2).cover?(argv.length)
16
+
17
+ app, macro_file = argv
18
+ macro_file ||= app.sub(/\.rb\z/, ".smacro.rb")
19
+ load File.expand_path(macro_file) if File.exist?(macro_file)
20
+ expanded = Smacro.expand(File.read(app))
21
+ options[:output] ? File.write(options[:output], expanded) : print(expanded)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module Smacro
2
+ VERSION = "0.1.0"
3
+ end
data/lib/smacro.rb ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # smacro: build-time macro expander for Spinel (PoC)
3
+ # ruby smacro.rb app.rb [app.smacro.rb] > app.expanded.rb
4
+ # spinel app.expanded.rb
5
+ require "prism"
6
+ require_relative "smacro/version"
7
+
8
+ module Smacro
9
+ MACROS = {}
10
+
11
+ def self.define(name, &blk)
12
+ MACROS[name] = blk
13
+ end
14
+
15
+ # AST literal -> Ruby value. Anything not known at build time is an error.
16
+ def self.const_value(node)
17
+ case node
18
+ when Prism::SymbolNode then node.unescaped.to_sym
19
+ when Prism::StringNode then node.unescaped
20
+ when Prism::IntegerNode then node.value
21
+ when Prism::TrueNode then true
22
+ when Prism::FalseNode then false
23
+ when Prism::NilNode then nil
24
+ when Prism::ArrayNode then node.elements.map { |e| const_value(e) }
25
+ else
26
+ raise ArgumentError,
27
+ "macro arg is not a compile-time literal (#{node.class}) at line #{node.location.start_line}"
28
+ end
29
+ end
30
+
31
+ class Collector < Prism::Visitor
32
+ attr_reader :hits
33
+
34
+ def initialize
35
+ @hits = []
36
+ end
37
+
38
+ def visit_call_node(node)
39
+ if node.receiver.nil? && node.block.nil? && MACROS.key?(node.name)
40
+ @hits << node # nested macros are not handled in the PoC
41
+ else
42
+ super
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.expand(src)
48
+ collector = Collector.new
49
+ Prism.parse(src).value.accept(collector)
50
+
51
+ bytes = src.b # Prism offsets are byte offsets
52
+ collector.hits.sort_by { |n| -n.location.start_offset }.each do |n|
53
+ args = (n.arguments ? n.arguments.arguments : []).map { |a| const_value(a) }
54
+ code = MACROS[n.name].call(*args).to_s
55
+ # Collapse to one line with ";" so every line number stays identical
56
+ # to the original file (free source map for error messages).
57
+ oneline = code.lines.map(&:strip).reject(&:empty?).join("; ")
58
+ bytes[n.location.start_offset, n.location.length] = oneline.b
59
+ end
60
+ bytes.force_encoding(Encoding::UTF_8)
61
+ end
62
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smacro
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - femto
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2026-09-28 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: prism
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
32
+ description: Expand Ruby source macros before compiling the result with Spinel.
33
+ executables:
34
+ - smacro
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - README.md
39
+ - exe/smacro
40
+ - lib/smacro.rb
41
+ - lib/smacro/cli.rb
42
+ - lib/smacro/version.rb
43
+ homepage: https://github.com/femto/smacro
44
+ licenses: []
45
+ metadata: {}
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.6.2
61
+ specification_version: 4
62
+ summary: Build-time Ruby macros for Spinel
63
+ test_files: []