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.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +13 -2
- data/README.md +101 -26
- data/Rakefile +7 -0
- data/lib/rlsl/base_translator/call_parser.rb +79 -0
- data/lib/rlsl/base_translator/code_rewriter.rb +107 -0
- data/lib/rlsl/base_translator/code_scanner.rb +140 -0
- data/lib/rlsl/base_translator.rb +182 -64
- data/lib/rlsl/code_generator/math_prelude.rb +172 -0
- data/lib/rlsl/code_generator/ruby_wrapper_generator.rb +137 -0
- data/lib/rlsl/code_generator/shader_function_generator.rb +19 -0
- data/lib/rlsl/code_generator/template_context.rb +44 -0
- data/lib/rlsl/code_generator/uniform_struct_generator.rb +29 -0
- data/lib/rlsl/code_generator.rb +30 -202
- data/lib/rlsl/compiled_shader.rb +5 -13
- data/lib/rlsl/errors.rb +29 -0
- data/lib/rlsl/function_context.rb +31 -14
- data/lib/rlsl/glsl/translator.rb +35 -41
- data/lib/rlsl/msl/shader.rb +34 -46
- data/lib/rlsl/msl/translator.rb +38 -34
- data/lib/rlsl/msl/uniform_buffer_packer.rb +72 -0
- data/lib/rlsl/prism/ast_visitor/control_flow_visiting.rb +137 -0
- data/lib/rlsl/prism/ast_visitor/definition_visiting.rb +92 -0
- data/lib/rlsl/prism/ast_visitor/expression_visiting.rb +172 -0
- data/lib/rlsl/prism/ast_visitor/scope_context.rb +48 -0
- data/lib/rlsl/prism/ast_visitor/visitor_registry.rb +21 -0
- data/lib/rlsl/prism/ast_visitor.rb +100 -286
- data/lib/rlsl/prism/builtins/function_registry.rb +131 -0
- data/lib/rlsl/prism/builtins/operator_rules.rb +123 -0
- data/lib/rlsl/prism/builtins/swizzle_rules.rb +47 -0
- data/lib/rlsl/prism/builtins.rb +39 -148
- data/lib/rlsl/prism/compilation_unit.rb +7 -0
- data/lib/rlsl/prism/emitters/base_emitter/control_flow_emission.rb +126 -0
- data/lib/rlsl/prism/emitters/base_emitter/definition_emission.rb +134 -0
- data/lib/rlsl/prism/emitters/base_emitter/expression_emission.rb +108 -0
- data/lib/rlsl/prism/emitters/base_emitter/statement_emission.rb +95 -0
- data/lib/rlsl/prism/emitters/base_emitter.rb +120 -414
- data/lib/rlsl/prism/emitters/c_emitter.rb +165 -112
- data/lib/rlsl/prism/emitters/glsl_emitter.rb +63 -50
- data/lib/rlsl/prism/emitters/msl_emitter.rb +67 -52
- data/lib/rlsl/prism/emitters/target_emitter.rb +124 -0
- data/lib/rlsl/prism/emitters/target_profile.rb +34 -0
- data/lib/rlsl/prism/emitters/wgsl_emitter.rb +217 -58
- data/lib/rlsl/prism/errors.rb +9 -0
- data/lib/rlsl/prism/ir/control_flow.rb +85 -0
- data/lib/rlsl/prism/ir/definitions.rb +82 -0
- data/lib/rlsl/prism/ir/expressions.rb +201 -0
- data/lib/rlsl/prism/ir/node.rb +21 -0
- data/lib/rlsl/prism/ir/nodes.rb +4 -371
- data/lib/rlsl/prism/ir/traversal.rb +66 -0
- data/lib/rlsl/prism/mutation_analyzer.rb +30 -0
- data/lib/rlsl/prism/node_traversal.rb +41 -0
- data/lib/rlsl/prism/parameter_list.rb +46 -0
- data/lib/rlsl/prism/return_flow_validator.rb +73 -0
- data/lib/rlsl/prism/source_extractor/block_locator.rb +50 -0
- data/lib/rlsl/prism/source_extractor.rb +19 -137
- data/lib/rlsl/prism/source_unit/parser.rb +88 -0
- data/lib/rlsl/prism/source_unit.rb +42 -0
- data/lib/rlsl/prism/target_capability_validator.rb +110 -0
- data/lib/rlsl/prism/transpiler.rb +99 -59
- data/lib/rlsl/prism/type_inference/call_type_resolver.rb +59 -0
- data/lib/rlsl/prism/type_inference/call_validator.rb +75 -0
- data/lib/rlsl/prism/type_inference/collection_type_resolver.rb +106 -0
- data/lib/rlsl/prism/type_inference/control_flow_inferer.rb +147 -0
- data/lib/rlsl/prism/type_inference/definition_inferer.rb +56 -0
- data/lib/rlsl/prism/type_inference/expression_inferer.rb +96 -0
- data/lib/rlsl/prism/type_inference/field_type_resolver.rb +38 -0
- data/lib/rlsl/prism/type_inference/inferer_registry.rb +38 -0
- data/lib/rlsl/prism/type_inference/scope_stack.rb +47 -0
- data/lib/rlsl/prism/type_inference/type_environment.rb +112 -0
- data/lib/rlsl/prism/type_inference/type_shapes.rb +33 -0
- data/lib/rlsl/prism/type_inference.rb +120 -249
- data/lib/rlsl/runtime_shader.rb +47 -0
- data/lib/rlsl/shader_builder/build_service.rb +104 -0
- data/lib/rlsl/shader_builder/native_extension_compiler.rb +97 -0
- data/lib/rlsl/shader_builder/shader_definition.rb +68 -0
- data/lib/rlsl/shader_builder/source_resolver.rb +109 -0
- data/lib/rlsl/shader_builder.rb +60 -111
- data/lib/rlsl/shader_name.rb +18 -0
- data/lib/rlsl/types/catalog.rb +47 -0
- data/lib/rlsl/types/target_resolver.rb +15 -0
- data/lib/rlsl/types/type_spec.rb +167 -0
- data/lib/rlsl/types/value_normalizer.rb +86 -0
- data/lib/rlsl/types.rb +9 -31
- data/lib/rlsl/uniform_context.rb +22 -11
- data/lib/rlsl/version.rb +1 -1
- data/lib/rlsl/wgsl/translator.rb +46 -39
- data/lib/rlsl/wgsl/uniform_layout.rb +25 -0
- data/lib/rlsl.rb +38 -15
- metadata +76 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f67d6c410b44bdc05706df3bb9bd923d89484a779ff379a8015ff7a4003333a4
|
|
4
|
+
data.tar.gz: 9231a24a3a255afb8e6512af7cd6e9fa5201e5b5bdf2fad914370f0b843c80b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6fb3852aa2f2e8910cba85f5904e8fe1de204d07544f0289bd9fba0c68c185b848b163616e5c37f53184fedb4cb62dd456a61fe36793ce450ef40de04fd7116
|
|
7
|
+
data.tar.gz: c52f07944ea36ca72e95ee5ea6425d5e2b410cfee054dd9ccfd23053b9b42851248f7eb93f49bfa8f8899acc953590ba7240911dd4e9dd961752a86a30d73522
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
-
|
|
1
|
+
<h1 align="center">RLSL</h1>
|
|
2
2
|
|
|
3
|
-
Ruby Like Shading Language
|
|
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&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
|
-
|
|
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 :
|
|
106
|
-
vec3 :
|
|
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
|
-
|
|
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
|
-
|
|
243
|
+
## Development
|
|
159
244
|
|
|
160
|
-
|
|
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
|
-
$
|
|
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
|
-
|
|
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
|
-
|
|
254
|
+
## Contributing
|
|
177
255
|
|
|
178
|
-
|
|
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
|