rlsl 0.1.1 → 1.0.1

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/CHANGELOG.md +13 -2
  4. data/README.md +101 -26
  5. data/Rakefile +7 -0
  6. data/lib/rlsl/base_translator/call_parser.rb +79 -0
  7. data/lib/rlsl/base_translator/code_rewriter.rb +107 -0
  8. data/lib/rlsl/base_translator/code_scanner.rb +140 -0
  9. data/lib/rlsl/base_translator.rb +182 -64
  10. data/lib/rlsl/code_generator/math_prelude.rb +172 -0
  11. data/lib/rlsl/code_generator/ruby_wrapper_generator.rb +137 -0
  12. data/lib/rlsl/code_generator/shader_function_generator.rb +19 -0
  13. data/lib/rlsl/code_generator/template_context.rb +44 -0
  14. data/lib/rlsl/code_generator/uniform_struct_generator.rb +29 -0
  15. data/lib/rlsl/code_generator.rb +30 -202
  16. data/lib/rlsl/compiled_shader.rb +5 -13
  17. data/lib/rlsl/errors.rb +29 -0
  18. data/lib/rlsl/function_context.rb +31 -14
  19. data/lib/rlsl/glsl/translator.rb +35 -41
  20. data/lib/rlsl/msl/shader.rb +34 -46
  21. data/lib/rlsl/msl/translator.rb +38 -34
  22. data/lib/rlsl/msl/uniform_buffer_packer.rb +72 -0
  23. data/lib/rlsl/prism/ast_visitor/control_flow_visiting.rb +137 -0
  24. data/lib/rlsl/prism/ast_visitor/definition_visiting.rb +92 -0
  25. data/lib/rlsl/prism/ast_visitor/expression_visiting.rb +172 -0
  26. data/lib/rlsl/prism/ast_visitor/scope_context.rb +48 -0
  27. data/lib/rlsl/prism/ast_visitor/visitor_registry.rb +21 -0
  28. data/lib/rlsl/prism/ast_visitor.rb +100 -286
  29. data/lib/rlsl/prism/builtins/function_registry.rb +131 -0
  30. data/lib/rlsl/prism/builtins/operator_rules.rb +123 -0
  31. data/lib/rlsl/prism/builtins/swizzle_rules.rb +47 -0
  32. data/lib/rlsl/prism/builtins.rb +39 -148
  33. data/lib/rlsl/prism/compilation_unit.rb +7 -0
  34. data/lib/rlsl/prism/emitters/base_emitter/control_flow_emission.rb +126 -0
  35. data/lib/rlsl/prism/emitters/base_emitter/definition_emission.rb +134 -0
  36. data/lib/rlsl/prism/emitters/base_emitter/expression_emission.rb +108 -0
  37. data/lib/rlsl/prism/emitters/base_emitter/statement_emission.rb +95 -0
  38. data/lib/rlsl/prism/emitters/base_emitter.rb +120 -414
  39. data/lib/rlsl/prism/emitters/c_emitter.rb +165 -112
  40. data/lib/rlsl/prism/emitters/glsl_emitter.rb +63 -50
  41. data/lib/rlsl/prism/emitters/msl_emitter.rb +67 -52
  42. data/lib/rlsl/prism/emitters/target_emitter.rb +124 -0
  43. data/lib/rlsl/prism/emitters/target_profile.rb +34 -0
  44. data/lib/rlsl/prism/emitters/wgsl_emitter.rb +217 -58
  45. data/lib/rlsl/prism/errors.rb +9 -0
  46. data/lib/rlsl/prism/ir/control_flow.rb +85 -0
  47. data/lib/rlsl/prism/ir/definitions.rb +82 -0
  48. data/lib/rlsl/prism/ir/expressions.rb +201 -0
  49. data/lib/rlsl/prism/ir/node.rb +21 -0
  50. data/lib/rlsl/prism/ir/nodes.rb +4 -371
  51. data/lib/rlsl/prism/ir/traversal.rb +66 -0
  52. data/lib/rlsl/prism/mutation_analyzer.rb +30 -0
  53. data/lib/rlsl/prism/node_traversal.rb +41 -0
  54. data/lib/rlsl/prism/parameter_list.rb +46 -0
  55. data/lib/rlsl/prism/return_flow_validator.rb +73 -0
  56. data/lib/rlsl/prism/source_extractor/block_locator.rb +50 -0
  57. data/lib/rlsl/prism/source_extractor.rb +19 -137
  58. data/lib/rlsl/prism/source_unit/parser.rb +88 -0
  59. data/lib/rlsl/prism/source_unit.rb +42 -0
  60. data/lib/rlsl/prism/target_capability_validator.rb +110 -0
  61. data/lib/rlsl/prism/transpiler.rb +99 -59
  62. data/lib/rlsl/prism/type_inference/call_type_resolver.rb +59 -0
  63. data/lib/rlsl/prism/type_inference/call_validator.rb +75 -0
  64. data/lib/rlsl/prism/type_inference/collection_type_resolver.rb +106 -0
  65. data/lib/rlsl/prism/type_inference/control_flow_inferer.rb +147 -0
  66. data/lib/rlsl/prism/type_inference/definition_inferer.rb +56 -0
  67. data/lib/rlsl/prism/type_inference/expression_inferer.rb +96 -0
  68. data/lib/rlsl/prism/type_inference/field_type_resolver.rb +38 -0
  69. data/lib/rlsl/prism/type_inference/inferer_registry.rb +38 -0
  70. data/lib/rlsl/prism/type_inference/scope_stack.rb +47 -0
  71. data/lib/rlsl/prism/type_inference/type_environment.rb +112 -0
  72. data/lib/rlsl/prism/type_inference/type_shapes.rb +33 -0
  73. data/lib/rlsl/prism/type_inference.rb +120 -249
  74. data/lib/rlsl/runtime_shader.rb +47 -0
  75. data/lib/rlsl/shader_builder/build_service.rb +104 -0
  76. data/lib/rlsl/shader_builder/native_extension_compiler.rb +97 -0
  77. data/lib/rlsl/shader_builder/shader_definition.rb +68 -0
  78. data/lib/rlsl/shader_builder/source_resolver.rb +109 -0
  79. data/lib/rlsl/shader_builder.rb +60 -111
  80. data/lib/rlsl/shader_name.rb +18 -0
  81. data/lib/rlsl/types/catalog.rb +47 -0
  82. data/lib/rlsl/types/target_resolver.rb +15 -0
  83. data/lib/rlsl/types/type_spec.rb +167 -0
  84. data/lib/rlsl/types/value_normalizer.rb +86 -0
  85. data/lib/rlsl/types.rb +9 -31
  86. data/lib/rlsl/uniform_context.rb +22 -11
  87. data/lib/rlsl/version.rb +1 -1
  88. data/lib/rlsl/wgsl/translator.rb +46 -39
  89. data/lib/rlsl/wgsl/uniform_layout.rb +25 -0
  90. data/lib/rlsl.rb +38 -15
  91. metadata +76 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0753b46ec16e2df8308ecab2b9e197cf24ab1d02f5028240d8fa6f9a886529b
4
- data.tar.gz: dcaf85ad34c2e2f4aa9479dbe3f405fd063d9d46c47966a1d4e137a8c4c513cd
3
+ metadata.gz: f67d6c410b44bdc05706df3bb9bd923d89484a779ff379a8015ff7a4003333a4
4
+ data.tar.gz: 9231a24a3a255afb8e6512af7cd6e9fa5201e5b5bdf2fad914370f0b843c80b2
5
5
  SHA512:
6
- metadata.gz: 13bd3732c03a7b6f0c0244ee63cc4d49ebba3264fa1dc6e8a78237b8e1d0c69ed15129787174871556e6afb8452d8cba6fb6c8bbac3f31a5488765a9866a1977
7
- data.tar.gz: e970cf8b44c96da77f52c0b6b68f578c896b710f2e5e41e5089726d652e762b51f1d6e3523393f4d31c355c9f3c845f7ca115dba2d1bba0e0e14d0e013dc06fc
6
+ metadata.gz: e6fb3852aa2f2e8910cba85f5904e8fe1de204d07544f0289bd9fba0c68c185b848b163616e5c37f53184fedb4cb62dd456a61fe36793ce450ef40de04fd7116
7
+ data.tar.gz: c52f07944ea36ca72e95ee5ea6425d5e2b410cfee054dd9ccfd23053b9b42851248f7eb93f49bfa8f8899acc953590ba7240911dd4e9dd961752a86a30d73522
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ NewCops: disable
3
+
4
+ # These class-level profile factories intentionally remain public even though
5
+ # their source location follows the protected instance API.
6
+ Lint/IneffectiveAccessModifier:
7
+ Exclude:
8
+ - lib/rlsl/base_translator.rb
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.0.1 - 2026-09-25
4
+
5
+ - Add Metal texture bindings and tighten shader typing and target validation.
6
+
7
+ ## 1.0.0 - 2026-04-05
8
+
9
+ - Breaking: custom translator and emitter integrations now require explicit target `PROFILE` definitions; the old stateful Prism transpiler API and compatibility aliases have been removed.
10
+ - Added: the Ruby shader DSL now supports ternary expressions, compound local assignments, and fragment transpilation that can reference globals/constants defined in Ruby helpers.
11
+ - Added: `functions` now supports validated signatures via `define`, plus shorthand declarations for `int`, `bool`, `mat2`, `mat3`, `mat4`, and `sampler2D`.
12
+ - Changed: compiled shaders and Metal shaders now validate and normalize uniform values at runtime, with clearer errors for missing uniforms and invalid scalar/vector/bool inputs.
13
+ - Fixed: integer type inference for custom function calls is preserved, and block source extraction is more reliable for assigned `if` expressions and modifier forms.
14
+ - Fixed: native extension builds and target capability errors are more robust, reducing backend-specific build and translation failures.
4
15
 
5
16
  ## 0.1.1 - 2026-01-04
6
17
 
@@ -8,4 +19,4 @@
8
19
 
9
20
  ## 0.1.0 - 2026-01-04
10
21
 
11
- - Initial release.
22
+ - Initial release.
data/README.md CHANGED
@@ -1,6 +1,19 @@
1
- # RLSL
1
+ <h1 align="center">RLSL</h1>
2
2
 
3
- Ruby Like Shading Language - A Ruby DSL for writing shaders that transpile to multiple GPU shader languages.
3
+ <p align="center">Ruby Like Shading Language — write shaders in Ruby and transpile them to GPU and CPU targets.</p>
4
+
5
+ <p align="center">
6
+ <a href="https://rubygems.org/gems/rlsl"><img src="https://badge.fury.io/rb/rlsl.svg" alt="Gem Version"></a>
7
+ <a href="https://rubygems.org/gems/rlsl"><img src="https://img.shields.io/gem/dt/rlsl?label=downloads" alt="Downloads"></a>
8
+ <a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%3E%3D3.1-CC342D?logo=ruby&amp;logoColor=white" alt="Ruby Version"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-750014.svg" alt="License"></a>
10
+ </p>
11
+
12
+ [Features](#features) · [Installation](#installation) · [Usage](#usage) · [Texture Resources](#texture-resources) · [Supported Types](#supported-types)
13
+
14
+ ***
15
+
16
+ RLSL is a Ruby DSL for shader authoring with type inference and target-specific code generation. Its C target supports CPU rendering.
4
17
 
5
18
  ## Features
6
19
 
@@ -33,6 +46,30 @@ Or install it yourself as:
33
46
  $ gem install rlsl
34
47
  ```
35
48
 
49
+ ### Requirements
50
+
51
+ - Ruby >= 3.1.0
52
+ - [Prism](https://github.com/ruby/prism) >= 1.0.0 (for Ruby parsing)
53
+ - A working Ruby C-extension toolchain for `RLSL.define` (`make` is selected from Ruby's `RbConfig`)
54
+
55
+ The test matrix covers supported Ruby releases on Ubuntu, macOS, and Windows, and runs a dedicated compatibility job against the declared Prism 1.0.0 lower bound. Metal runtime execution is macOS-only. GLSL, WGSL, and MSL source generation is platform-independent.
56
+
57
+ ### Optional: Metal Shader Execution (macOS only)
58
+
59
+ To run Metal shaders natively, install the [metaco](https://github.com/rbgfx/metaco) gem separately:
60
+
61
+ ```bash
62
+ $ gem install metaco
63
+ ```
64
+
65
+ Or add to your Gemfile:
66
+
67
+ ```ruby
68
+ gem "metaco", platforms: :ruby, install_if: -> { RUBY_PLATFORM.include?("darwin") }
69
+ ```
70
+
71
+ MSL code generation (`RLSL.to_msl` or `RLSL.define_metal`) works without metaco. The gem is only required when calling `render_metal` at runtime.
72
+
36
73
  ## Usage
37
74
 
38
75
  ### Basic Example
@@ -82,7 +119,7 @@ end
82
119
  ### Generate MSL (Metal)
83
120
 
84
121
  ```ruby
85
- metal_shader = RLSL.define_metal(:my_shader) do
122
+ msl_code = RLSL.to_msl(:my_shader) do
86
123
  uniforms do
87
124
  float :time
88
125
  end
@@ -93,6 +130,8 @@ metal_shader = RLSL.define_metal(:my_shader) do
93
130
  end
94
131
  ```
95
132
 
133
+ `RLSL.to_glsl`, `RLSL.to_wgsl`, and `RLSL.to_msl` return source strings. `RLSL.define` compiles the C target for CPU rendering, while `RLSL.define_metal` returns an `RLSL::MSL::Shader` for optional Metal execution.
134
+
96
135
  ### Using Helper Functions
97
136
 
98
137
  ```ruby
@@ -102,8 +141,8 @@ RLSL.to_glsl(:complex_shader) do
102
141
  end
103
142
 
104
143
  functions do
105
- float :noise
106
- vec3 :get_color
144
+ define :noise, returns: :float, params: { p: :vec2 }
145
+ define :get_color, returns: :vec3, params: { uv: :vec2, t: :float }
107
146
  end
108
147
 
109
148
  helpers(:ruby) do
@@ -123,6 +162,53 @@ RLSL.to_glsl(:complex_shader) do
123
162
  end
124
163
  ```
125
164
 
165
+ Every Ruby helper function needs a complete declaration in `functions`; parameter names and order must match the Ruby method definition. This prevents unknown parameters from silently becoming floats.
166
+
167
+ ### Fragment Parameters and Explicit Source
168
+
169
+ Fragment parameters are positional. The first parameter is the fragment coordinate (`vec2`), the second is the resolution (`vec2`), and the third is the uniform object. Their Ruby names may be changed; generated target code uses the canonical names `frag_coord`, `resolution`, and `u`.
170
+
171
+ Ruby-mode blocks are captured from their source file when `fragment` or `helpers` is declared. They must come from a readable file, and multiple shader blocks must not start on the same source line. Code created by `eval`, `ruby -e`, or some REPLs has no readable source file. Once declared, later edits to the file do not change the captured shader source.
172
+
173
+ For generated code and REPL use, provide the Ruby source explicitly with `fragment_source` or `helpers_source`:
174
+
175
+ ```ruby
176
+ builder = RLSL::ShaderBuilder.new(:generated_shader)
177
+ builder.fragment_source <<~RUBY
178
+ |coordinate, size, uniforms|
179
+ uv = coordinate / size
180
+ vec3(uv.x, uv.y, uniforms.time)
181
+ RUBY
182
+ ```
183
+
184
+ A block with parameters is automatically treated as Ruby shader code. A no-argument block defaults to legacy C-source mode; use `fragment(:ruby) { vec3(1.0, 0.0, 0.0) }` to select Ruby mode explicitly. Raw helper or fragment source can be selected with `helpers(:c)` and `fragment(:c)`.
185
+
186
+ WGSL generation supports Ruby shader source only. Legacy C snippets can still be translated to C, GLSL, and MSL, but `to_wgsl` rejects them because C declarations and function syntax cannot be converted into valid WGSL by identifier rewriting alone.
187
+
188
+ Use `builder.uniform_types` to inspect declared uniform types. The block form of `uniforms` remains the declaration API.
189
+
190
+ Uniform names must be ASCII identifiers and may not use the RLSL-reserved names `resolution`, `frag_coord`, or `u`. Duplicate uniform declarations are rejected. The generated GLSL uniform block uses `std140` at binding 1.
191
+
192
+ ### Texture Resources
193
+
194
+ `sampler2D` uniforms are source-generation resources for GLSL, WGSL, and MSL. Use `texture`, `texture2D`, or `textureLod` in Ruby shader code. They are not fields in the uniform buffer:
195
+
196
+ - GLSL reserves binding 0 for output and binding 1 for the uniform block; textures begin at binding 2.
197
+ - WGSL uses group 0 bindings 0 and 1 for the uniform buffer and output. Each texture/sampler pair then uses bindings 2/3, 4/5, and so on.
198
+ - MSL uses texture 0 for output, texture 1 onward for sampled textures, and buffer 0 for uniforms. The generated shader uses an internal linear sampler.
199
+
200
+ The C renderer does not bind `sampler2D` resources; attempts to use texture
201
+ functions on C are rejected with a target-capability error. Metal runtime
202
+ rendering accepts a `textures:` hash keyed by sampler name and binds each
203
+ texture in declaration order:
204
+
205
+ ```ruby
206
+ shader.render_metal(handle, width, height, uniforms, textures: { albedo: texture })
207
+ ```
208
+
209
+ Texture-enabled source can also be emitted with `to_glsl`, `to_wgsl`, or
210
+ `to_msl` and bound by the host application.
211
+
126
212
  ## Supported Types
127
213
 
128
214
  - `bool` - Boolean (conditional logic)
@@ -136,6 +222,8 @@ end
136
222
  - `mat2` - 2x2 matrix (2D texture coordinate transformations)
137
223
  - `sampler2D` - 2D texture sampler
138
224
 
225
+ Scalar and vector uniforms (`bool`, `int`, `float`, and `vec2` through `vec4`) are supported by the CPU and Metal runtime packers. Matrix and sampler uniforms are source-generation only.
226
+
139
227
  ## Built-in Functions
140
228
 
141
229
  RLSL supports common shader functions:
@@ -150,35 +238,22 @@ RLSL supports common shader functions:
150
238
  - `PI` - 3.14159265358979323846
151
239
  - `TAU` - 6.28318530717958647692
152
240
 
153
- ## Requirements
154
-
155
- - Ruby >= 3.1.0
156
- - [Prism](https://github.com/ruby/prism) gem (for Ruby parsing)
241
+ Ruby-style `Math::PI` and `Math::TAU` are accepted as aliases in shader source.
157
242
 
158
- ### Optional: Metal Shader Execution (macOS only)
243
+ ## Development
159
244
 
160
- To run Metal shaders natively, install the [metaco](https://github.com/ydah/metaco) gem separately:
245
+ After checking out the repo, run `bundle install` to install dependencies. Then run `rake verify` for lint and tests.
161
246
 
162
247
  ```bash
163
- $ gem install metaco
164
- ```
165
-
166
- Or add to your Gemfile:
167
-
168
- ```ruby
169
- gem "metaco", platforms: :ruby, install_if: -> { RUBY_PLATFORM.include?("darwin") }
248
+ $ bundle install
249
+ $ rake verify
170
250
  ```
171
251
 
172
- MSL code generation (`RLSL.define_metal`) works without metaco. The gem is only required when calling `render_metal` at runtime.
173
-
174
- ## Development
252
+ For a quick local coverage summary, run `COVERAGE=1 rake test`. Set `COVERAGE_MIN=85` to enforce a minimum percentage. Target compiler integration tests use `glslangValidator`, Naga's `naga` CLI, and `xcrun metal` when those tools are installed; CI requires the GLSL and WGSL validators.
175
253
 
176
- After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests.
254
+ ## Contributing
177
255
 
178
- ```bash
179
- $ bundle install
180
- $ rake test
181
- ```
256
+ Bug reports and pull requests are welcome at [rbgfx/rlsl](https://github.com/rbgfx/rlsl).
182
257
 
183
258
  ## License
184
259
 
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ require "rubocop/rake_task"
5
6
 
6
7
  Rake::TestTask.new(:test) do |t|
7
8
  t.libs << "test"
@@ -10,3 +11,9 @@ Rake::TestTask.new(:test) do |t|
10
11
  end
11
12
 
12
13
  task default: :test
14
+
15
+ RuboCop::RakeTask.new(:lint) do |task|
16
+ task.options = ["--cache", "false", "--only", "Lint,Security", "lib"]
17
+ end
18
+
19
+ task verify: %i[lint test]
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLSL
4
+ class BaseTranslator
5
+ class CallParser
6
+ def self.extract_group(segment, opening_index)
7
+ depth = 0
8
+ index = opening_index
9
+ quote = nil
10
+
11
+ while index < segment.length
12
+ char = segment[index]
13
+
14
+ if quote
15
+ quote = nil if char == quote && !escaped?(segment, index)
16
+ else
17
+ case char
18
+ when '"', "'"
19
+ quote = char
20
+ when "(", "[", "{"
21
+ depth += 1
22
+ when ")", "]", "}"
23
+ depth -= 1
24
+ return [segment[(opening_index + 1)...index], index] if depth.zero?
25
+ end
26
+ end
27
+
28
+ index += 1
29
+ end
30
+
31
+ raise ArgumentError, "Unbalanced delimiter in translation source"
32
+ end
33
+
34
+ def self.split_arguments(arguments_source)
35
+ arguments = []
36
+ depth = 0
37
+ quote = nil
38
+ start_index = 0
39
+
40
+ arguments_source.each_char.with_index do |char, index|
41
+ if quote
42
+ quote = nil if char == quote && !escaped?(arguments_source, index)
43
+ next
44
+ end
45
+
46
+ case char
47
+ when '"', "'"
48
+ quote = char
49
+ when "(", "[", "{"
50
+ depth += 1
51
+ when ")", "]", "}"
52
+ depth -= 1
53
+ when ","
54
+ next unless depth.zero?
55
+
56
+ arguments << arguments_source[start_index...index]
57
+ start_index = index + 1
58
+ end
59
+ end
60
+
61
+ tail = arguments_source[start_index..]
62
+ return [] if arguments.empty? && tail.to_s.strip.empty?
63
+
64
+ arguments << tail
65
+ end
66
+
67
+ def self.escaped?(source, index)
68
+ backslashes = 0
69
+ cursor = index - 1
70
+ while cursor >= 0 && source[cursor] == "\\"
71
+ backslashes += 1
72
+ cursor -= 1
73
+ end
74
+
75
+ backslashes.odd?
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "call_parser"
4
+ require_relative "code_scanner"
5
+
6
+ module RLSL
7
+ class BaseTranslator
8
+ class CodeRewriter
9
+ IDENTIFIER_PATTERN = /[A-Za-z_][A-Za-z0-9_]*/
10
+
11
+ def initialize(code)
12
+ @code = code.to_s
13
+ end
14
+
15
+ def rewrite(identifier_replacements:, call_rewrites:, removed_identifiers:)
16
+ rewrite_segment(
17
+ @code,
18
+ identifier_replacements: stringify_keys(identifier_replacements),
19
+ call_rewrites: stringify_keys(call_rewrites),
20
+ removed_identifiers: Array(removed_identifiers).map(&:to_s)
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ def rewrite_segment(segment, identifier_replacements:, call_rewrites:, removed_identifiers:)
27
+ CodeScanner.new(segment).each_segment.map do |code_segment|
28
+ code_segment.code? ? rewrite_code_segment(
29
+ code_segment.text,
30
+ identifier_replacements: identifier_replacements,
31
+ call_rewrites: call_rewrites,
32
+ removed_identifiers: removed_identifiers
33
+ ) : code_segment.text
34
+ end.join
35
+ end
36
+
37
+ def rewrite_code_segment(segment, identifier_replacements:, call_rewrites:, removed_identifiers:)
38
+ output = +""
39
+ index = 0
40
+
41
+ while index < segment.length
42
+ if identifier_start?(segment, index)
43
+ identifier, identifier_end = read_identifier(segment, index)
44
+ call_index = skip_whitespace(segment, identifier_end)
45
+
46
+ if call_rewrites.key?(identifier) && segment[call_index] == "("
47
+ arguments_source, closing_index = CallParser.extract_group(segment, call_index)
48
+ rewritten_args = CallParser.split_arguments(arguments_source).map do |argument|
49
+ rewrite_segment(
50
+ argument,
51
+ identifier_replacements: identifier_replacements,
52
+ call_rewrites: call_rewrites,
53
+ removed_identifiers: removed_identifiers
54
+ ).strip
55
+ end
56
+ output << render_call(call_rewrites.fetch(identifier), rewritten_args)
57
+ index = closing_index + 1
58
+ next
59
+ end
60
+
61
+ unless removed_identifiers.include?(identifier)
62
+ output << (identifier_replacements[identifier] || identifier)
63
+ end
64
+ index = identifier_end
65
+ next
66
+ end
67
+
68
+ output << segment[index]
69
+ index += 1
70
+ end
71
+
72
+ output
73
+ end
74
+
75
+ def render_call(rewriter, arguments)
76
+ return rewriter.call(arguments) if rewriter.respond_to?(:call)
77
+
78
+ "#{rewriter}(#{arguments.join(', ')})"
79
+ end
80
+
81
+ def stringify_keys(hash)
82
+ hash.each_with_object({}) do |(key, value), memo|
83
+ memo[key.to_s] = value
84
+ end
85
+ end
86
+
87
+ def identifier_start?(segment, index)
88
+ segment[index] =~ /[A-Za-z_]/
89
+ end
90
+
91
+ def read_identifier(segment, index)
92
+ match = IDENTIFIER_PATTERN.match(segment, index)
93
+ [match[0], match.end(0)]
94
+ end
95
+
96
+ def skip_whitespace(segment, index)
97
+ current_index = index
98
+ current_index += 1 while current_index < segment.length && whitespace?(segment[current_index])
99
+ current_index
100
+ end
101
+
102
+ def whitespace?(char)
103
+ char =~ /\s/
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLSL
4
+ class BaseTranslator
5
+ class CodeScanner
6
+ Segment = Struct.new(:type, :text, keyword_init: true) do
7
+ def code?
8
+ type == :code
9
+ end
10
+ end
11
+
12
+ def initialize(code)
13
+ @code = code.to_s
14
+ end
15
+
16
+ def each_segment
17
+ return enum_for(:each_segment) unless block_given?
18
+
19
+ buffer = +""
20
+ index = 0
21
+
22
+ while index < @code.length
23
+ preserved, next_index = read_preserved_segment(index)
24
+ unless preserved
25
+ buffer << @code[index]
26
+ index += 1
27
+ next
28
+ end
29
+
30
+ yield Segment.new(type: :code, text: buffer) unless buffer.empty?
31
+ buffer = +""
32
+ yield Segment.new(type: preserved[:type], text: preserved[:text])
33
+ index = next_index
34
+ end
35
+
36
+ yield Segment.new(type: :code, text: buffer) unless buffer.empty?
37
+ end
38
+
39
+ private
40
+
41
+ def read_preserved_segment(index)
42
+ if preprocessor_start?(index)
43
+ directive, next_index = read_preprocessor(index)
44
+ return [{ type: :preprocessor, text: directive }, next_index]
45
+ end
46
+
47
+ if line_comment_start?(index)
48
+ comment, next_index = read_line_comment(index)
49
+ return [{ type: :line_comment, text: comment }, next_index]
50
+ end
51
+
52
+ if block_comment_start?(index)
53
+ comment, next_index = read_block_comment(index)
54
+ return [{ type: :block_comment, text: comment }, next_index]
55
+ end
56
+
57
+ if string_delimiter?(@code[index])
58
+ literal, next_index = read_quoted(index)
59
+ return [{ type: :string, text: literal }, next_index]
60
+ end
61
+
62
+ nil
63
+ end
64
+
65
+ def preprocessor_start?(index)
66
+ return false unless @code[index] == "#"
67
+
68
+ line_start = @code.rindex("\n", index - 1)
69
+ prefix = @code[(line_start ? line_start + 1 : 0)...index]
70
+ prefix.match?(/\A[ \t]*\z/)
71
+ end
72
+
73
+ def read_preprocessor(index)
74
+ cursor = index
75
+
76
+ loop do
77
+ newline_index = @code.index("\n", cursor)
78
+ return [@code[index..], @code.length] unless newline_index
79
+
80
+ line = @code[cursor...newline_index]
81
+ return [@code[index...newline_index], newline_index] unless line.rstrip.end_with?("\\")
82
+
83
+ cursor = newline_index + 1
84
+ end
85
+ end
86
+
87
+ def line_comment_start?(index)
88
+ @code[index, 2] == "//"
89
+ end
90
+
91
+ def block_comment_start?(index)
92
+ @code[index, 2] == "/*"
93
+ end
94
+
95
+ def string_delimiter?(char)
96
+ char == '"' || char == "'"
97
+ end
98
+
99
+ def read_line_comment(index)
100
+ newline_index = @code.index("\n", index)
101
+ end_index = newline_index ? newline_index : @code.length
102
+ [@code[index...end_index], end_index]
103
+ end
104
+
105
+ def read_block_comment(index)
106
+ closing_index = @code.index("*/", index + 2)
107
+ raise ArgumentError, "Unterminated block comment in translation source" unless closing_index
108
+
109
+ end_index = closing_index + 2
110
+ [@code[index...end_index], end_index]
111
+ end
112
+
113
+ def read_quoted(index)
114
+ quote = @code[index]
115
+ cursor = index + 1
116
+
117
+ while cursor < @code.length
118
+ if @code[cursor] == quote && !escaped?(cursor)
119
+ return [@code[index..cursor], cursor + 1]
120
+ end
121
+
122
+ cursor += 1
123
+ end
124
+
125
+ raise ArgumentError, "Unterminated string literal in translation source"
126
+ end
127
+
128
+ def escaped?(index)
129
+ backslashes = 0
130
+ cursor = index - 1
131
+ while cursor >= 0 && @code[cursor] == "\\"
132
+ backslashes += 1
133
+ cursor -= 1
134
+ end
135
+
136
+ backslashes.odd?
137
+ end
138
+ end
139
+ end
140
+ end