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
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLSL
4
+ module Prism
5
+ module NodeTraversal
6
+ module_function
7
+
8
+ def each(node)
9
+ return enum_for(__method__, node) unless block_given?
10
+ return unless node
11
+
12
+ stack = [node]
13
+ until stack.empty?
14
+ current = stack.pop
15
+ yield current
16
+ stack.concat(child_nodes(current).reverse)
17
+ end
18
+ end
19
+
20
+ def depth_exceeds?(node, maximum)
21
+ return false unless node
22
+
23
+ stack = [[node, 1]]
24
+ until stack.empty?
25
+ current, depth = stack.pop
26
+ return true if depth > maximum
27
+
28
+ child_nodes(current).reverse_each { |child| stack << [child, depth + 1] }
29
+ end
30
+ false
31
+ end
32
+
33
+ def child_nodes(node)
34
+ return node.compact_child_nodes if node.respond_to?(:compact_child_nodes)
35
+
36
+ Array(node.child_nodes).compact
37
+ end
38
+ private_class_method :child_nodes
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module RLSL
6
+ module Prism
7
+ module ParameterList
8
+ module_function
9
+
10
+ def required_names(parameter_container)
11
+ parameters = unwrap(parameter_container)
12
+ return [] unless parameters
13
+
14
+ unsupported = children(parameters).reject { |parameter| parameter.is_a?(::Prism::RequiredParameterNode) }
15
+ unless unsupported.empty?
16
+ raise UnsupportedSyntaxError, "Only required positional parameters are supported"
17
+ end
18
+
19
+ parameters.requireds.map(&:name)
20
+ end
21
+
22
+ def names(parameter_container)
23
+ parameters = unwrap(parameter_container)
24
+ return [] unless parameters
25
+
26
+ children(parameters).filter_map { |parameter| parameter.name if parameter.respond_to?(:name) }
27
+ end
28
+
29
+ def unwrap(container)
30
+ return container.parameters if container.respond_to?(:parameters)
31
+
32
+ container
33
+ end
34
+ private_class_method :unwrap
35
+
36
+ def children(parameters)
37
+ if parameters.respond_to?(:compact_child_nodes)
38
+ parameters.compact_child_nodes
39
+ else
40
+ Array(parameters.child_nodes).compact
41
+ end
42
+ end
43
+ private_class_method :children
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ir/traversal"
4
+
5
+ module RLSL
6
+ module Prism
7
+ class ReturnFlowError < RLSL::Error; end
8
+
9
+ class ReturnFlowValidator
10
+ VALUE_NODES = [
11
+ IR::VarDecl,
12
+ IR::Assignment,
13
+ IR::VarRef,
14
+ IR::Literal,
15
+ IR::BoolLiteral,
16
+ IR::BinaryOp,
17
+ IR::UnaryOp,
18
+ IR::FuncCall,
19
+ IR::FieldAccess,
20
+ IR::Swizzle,
21
+ IR::Ternary,
22
+ IR::Constant,
23
+ IR::Parenthesized,
24
+ IR::ArrayIndex
25
+ ].freeze
26
+
27
+ def validate!(node, needs_return:)
28
+ validate_functions!(node)
29
+ validate_returning_block!(node, "shader fragment") if needs_return
30
+ node
31
+ end
32
+
33
+ private
34
+
35
+ def validate_functions!(node)
36
+ IR::Traversal.each(node) do |current|
37
+ next unless current.is_a?(IR::FunctionDefinition)
38
+
39
+ validate_returning_block!(
40
+ current.body,
41
+ "function #{current.name}",
42
+ tuple_return: current.return_type.is_a?(Array)
43
+ )
44
+ end
45
+ end
46
+
47
+ def validate_returning_block!(node, context, tuple_return: false)
48
+ return if returns_value_on_all_paths?(node, tuple_return: tuple_return)
49
+
50
+ raise ReturnFlowError.new(
51
+ "#{context} does not return a value on every path"
52
+ ).with_source_location(node.location)
53
+ end
54
+
55
+ def returns_value_on_all_paths?(node, tuple_return: false)
56
+ case node
57
+ when IR::Block
58
+ returns_value_on_all_paths?(node.statements.last, tuple_return: tuple_return)
59
+ when IR::Return
60
+ !node.expression.nil? && (tuple_return || !node.expression.is_a?(IR::ArrayLiteral))
61
+ when IR::IfStatement
62
+ node.else_branch &&
63
+ returns_value_on_all_paths?(node.then_branch, tuple_return: tuple_return) &&
64
+ returns_value_on_all_paths?(node.else_branch, tuple_return: tuple_return)
65
+ when IR::ArrayLiteral
66
+ tuple_return
67
+ else
68
+ VALUE_NODES.any? { |klass| node.is_a?(klass) }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../node_traversal"
4
+ require_relative "../parameter_list"
5
+
6
+ module RLSL
7
+ module Prism
8
+ class SourceExtractor
9
+ class BlockLocator
10
+ def extract(source, start_line)
11
+ extract_unit(source, start_line).to_source
12
+ end
13
+
14
+ def extract_unit(source, start_line, parameters: nil, source_name: "(shader block)")
15
+ parsed = ::Prism.parse(source)
16
+ raise SourceNotAvailable, "Unable to parse block source" unless parsed.success?
17
+
18
+ block = block_at_line(parsed.value, start_line, parameters)
19
+ raise SourceNotAvailable, "Unable to locate block source" unless block
20
+
21
+ SourceUnit.from_block(block, source_name: source_name)
22
+ end
23
+
24
+ private
25
+
26
+ def block_at_line(node, start_line, parameters)
27
+ candidates = NodeTraversal.each(node).select do |current|
28
+ current.is_a?(::Prism::BlockNode) && current.location.start_line == start_line
29
+ end
30
+ candidates.select! { |candidate| parameter_names(candidate) == required_parameter_names(parameters) } if parameters
31
+
32
+ if candidates.length > 1
33
+ raise SourceNotAvailable,
34
+ "Multiple shader blocks start on line #{start_line}; put each block on its own line"
35
+ end
36
+
37
+ candidates.first
38
+ end
39
+
40
+ def parameter_names(block)
41
+ ParameterList.names(block.parameters)
42
+ end
43
+
44
+ def required_parameter_names(parameters)
45
+ Array(parameters).filter_map { |_kind, name| name }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,151 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "prism"
4
+
5
+ require_relative "source_unit"
6
+ require_relative "source_extractor/block_locator"
7
+
3
8
  module RLSL
4
9
  module Prism
5
10
  class SourceExtractor
6
- class SourceNotAvailable < StandardError; end
7
-
8
- def extract(block)
9
- file, line_num = block.source_location
10
- raise SourceNotAvailable, "Block source location not available" unless file && File.exist?(file)
11
-
12
- lines = File.readlines(file)
13
- extract_block_source(lines, line_num - 1)
14
- end
15
-
16
- def extract_from_string(source)
17
- source
18
- end
19
-
20
- private
21
-
22
- def extract_block_source(lines, start_line)
23
- source = +""
24
- depth = 0
25
- in_block = false
26
- block_start_found = false
27
-
28
- lines[start_line..].each_with_index do |line, idx|
29
- tokens = tokenize_for_blocks(line)
30
-
31
- tokens.each do |token|
32
- case token
33
- when :do, :brace_open
34
- if !block_start_found
35
- block_start_found = true
36
- in_block = true
37
- end
38
- depth += 1
39
- when :block_start
40
- depth += 1
41
- when :end, :brace_close
42
- depth -= 1
43
- end
44
- end
45
-
46
- if block_start_found
47
- if idx == 0
48
- source << extract_first_line(line)
49
- else
50
- source << line
51
- end
52
- end
53
-
54
- break if in_block && depth == 0
55
- end
11
+ class SourceNotAvailable < RLSL::Error; end
56
12
 
57
- clean_block_source(source)
13
+ def initialize(block_locator = BlockLocator.new)
14
+ @block_locator = block_locator
58
15
  end
59
16
 
60
- def tokenize_for_blocks(line)
61
- tokens = []
62
- in_string = nil
63
- i = 0
64
-
65
- while i < line.length
66
- char = line[i]
67
-
68
- if in_string
69
- if char == in_string && (i == 0 || line[i - 1] != "\\")
70
- in_string = nil
71
- end
72
- i += 1
73
- next
74
- end
75
-
76
- if char == '"' || char == "'"
77
- in_string = char
78
- i += 1
79
- next
80
- end
81
-
82
- break if char == "#"
83
-
84
- prev_is_boundary = i == 0 || !line[i - 1].match?(/[a-zA-Z0-9_]/)
85
-
86
- if char == "{"
87
- tokens << :brace_open
88
- elsif char == "}"
89
- tokens << :brace_close
90
- elsif prev_is_boundary && line[i..].match?(/\Ado\b/)
91
- tokens << :do
92
- i += 1
93
- elsif prev_is_boundary && line[i..].match?(/\Aelsif\b/)
94
- i += 4
95
- elsif prev_is_boundary && line[i..].match?(/\Aelse\b/)
96
- i += 3
97
- elsif prev_is_boundary && (m = line[i..].match(/\A(if|unless|while|for|case|def|class|module)\b/))
98
- tokens << :block_start
99
- i += m[1].length - 1
100
- elsif prev_is_boundary && line[i..].match?(/\Aend\b/)
101
- tokens << :end
102
- i += 2
103
- end
104
-
105
- i += 1
106
- end
107
-
108
- tokens
109
- end
110
-
111
- def extract_first_line(line)
112
- if line.include?(" do")
113
- match = line.match(/do\s*(\|[^|]*\|)?\s*(.*)$/)
114
- if match
115
- params = match[1] || ""
116
- rest = match[2] || ""
117
- "#{params}\n#{rest}\n"
118
- else
119
- "\n"
120
- end
121
- elsif line.include?("{")
122
- match = line.match(/\{\s*(\|[^|]*\|)?\s*(.*)$/)
123
- if match
124
- params = match[1] || ""
125
- rest = match[2] || ""
126
- "#{params}\n#{rest}\n"
127
- else
128
- "\n"
129
- end
130
- else
131
- line
132
- end
17
+ def extract(block)
18
+ extract_unit(block).to_source
133
19
  end
134
20
 
135
- def clean_block_source(source)
136
- lines = source.lines
137
- return "" if lines.empty?
138
-
139
- last_line = lines.last.strip
140
- if last_line == "end" || last_line == "}"
141
- lines.pop
142
- elsif last_line.end_with?("end") || last_line.end_with?("}")
143
- lines[-1] = lines[-1].sub(/\s*(end|\})\s*$/, "\n")
144
- end
145
-
146
- lines.shift if lines.first&.strip&.empty?
21
+ def extract_unit(block)
22
+ file, line_num = block.source_location
23
+ raise SourceNotAvailable, "Block source location not available" unless file && File.exist?(file)
147
24
 
148
- lines.join
25
+ @block_locator.extract_unit(
26
+ File.read(file),
27
+ line_num,
28
+ parameters: block.parameters,
29
+ source_name: file
30
+ )
149
31
  end
150
32
  end
151
33
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../node_traversal"
6
+ require_relative "../parameter_list"
7
+
8
+ module RLSL
9
+ module Prism
10
+ class SourceUnitParser
11
+ def initialize(source, source_name: "(shader source)")
12
+ @source = source.to_s
13
+ @source_name = source_name
14
+ end
15
+
16
+ def parse
17
+ normalized, leading_line_offset = strip_with_line_offset(@source)
18
+ if normalized.empty?
19
+ return SourceUnit.new(params: [], body: "", source_name: @source_name, line_offset: leading_line_offset)
20
+ end
21
+
22
+ params_source, body_source, parameter_line_offset = split_sections(normalized)
23
+ stripped_body, body_line_offset = strip_with_line_offset(body_source)
24
+ line_offset = leading_line_offset + parameter_line_offset + body_line_offset
25
+ validate_body!(stripped_body, line_offset)
26
+
27
+ SourceUnit.new(
28
+ params: parse_params(params_source),
29
+ body: stripped_body,
30
+ source_name: @source_name,
31
+ line_offset: line_offset
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def split_sections(source)
38
+ lines = source.lines
39
+ first_line = lines.first&.strip
40
+ return [nil, source, 0] unless parameter_line?(first_line)
41
+
42
+ [first_line, lines[1..].to_a.join, 1]
43
+ end
44
+
45
+ def parameter_line?(line)
46
+ line&.start_with?("|") && line.end_with?("|")
47
+ end
48
+
49
+ def validate_body!(body_source, line_offset)
50
+ return if body_source.to_s.strip.empty?
51
+
52
+ parsed = ::Prism.parse(body_source)
53
+ return if parsed.success?
54
+
55
+ error = RLSL::ParseError.new("Unable to parse source unit body")
56
+ location = parsed.errors.first&.location
57
+ if location
58
+ error.with_source_location(
59
+ RLSL::SourceLocation.new(
60
+ source_name: @source_name,
61
+ line: line_offset + location.start_line,
62
+ column: location.start_column + 1
63
+ )
64
+ )
65
+ end
66
+ raise error
67
+ end
68
+
69
+ def parse_params(params_source)
70
+ return [] unless params_source
71
+
72
+ parsed = ::Prism.parse("proc do #{params_source}\nend\n")
73
+ raise RLSL::ParseError, "Unable to parse source unit params" unless parsed.success?
74
+
75
+ block = NodeTraversal.each(parsed.value).find { |node| node.is_a?(::Prism::BlockNode) }
76
+ return [] unless block
77
+
78
+ ParameterList.required_names(block.parameters)
79
+ end
80
+
81
+ def strip_with_line_offset(source)
82
+ text = source.to_s
83
+ leading_whitespace = text[/\A\s*/].to_s
84
+ [text.strip, leading_whitespace.count("\n")]
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "source_unit/parser"
4
+ require_relative "parameter_list"
5
+
6
+ module RLSL
7
+ module Prism
8
+ SourceUnit = Struct.new(:params, :body, :source_name, :line_offset, keyword_init: true) do
9
+ class << self
10
+ def from_source(source, source_name: "(shader source)")
11
+ SourceUnitParser.new(source, source_name: source_name).parse
12
+ end
13
+
14
+ def from_block(block, source_name: "(shader block)")
15
+ new(
16
+ params: extract_params(block),
17
+ body: block.body&.slice.to_s.strip,
18
+ source_name: source_name,
19
+ line_offset: block.body ? block.body.location.start_line - 1 : block.location.start_line - 1
20
+ )
21
+ end
22
+
23
+ private
24
+
25
+ def extract_params(block)
26
+ ParameterList.required_names(block.parameters)
27
+ end
28
+ end
29
+
30
+ def without_params
31
+ self.class.new(params: [], body: body, source_name: source_name, line_offset: line_offset)
32
+ end
33
+
34
+ def to_source
35
+ segments = []
36
+ segments << "|#{params.join(', ')}|" unless params.empty?
37
+ segments << body unless body.empty?
38
+ segments.join("\n")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../types"
4
+ require_relative "ir/traversal"
5
+ require_relative "type_inference/type_shapes"
6
+
7
+ module RLSL
8
+ module Prism
9
+ class TargetCapabilityError < RLSL::Error; end
10
+
11
+ class TargetCapabilityValidator
12
+ include TypeShapes
13
+
14
+ def validate!(node, target)
15
+ @target = target.to_sym
16
+ IR::Traversal.each(node) do |current|
17
+ validate_node!(current)
18
+ rescue RLSL::Error => error
19
+ error.with_source_location(current.location)
20
+ raise
21
+ end
22
+ node
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :target
28
+
29
+ def validate_node!(node)
30
+ return if node.nil?
31
+
32
+ validate_type!(node.type, context: node.class.name.split("::").last)
33
+
34
+ case node
35
+ when IR::FuncCall
36
+ validate_builtin!(node)
37
+ when IR::FunctionDefinition
38
+ validate_type!(node.return_type, context: "function #{node.name} return")
39
+ node.param_types.each_value do |type|
40
+ validate_type!(type, context: "function #{node.name} parameter")
41
+ end
42
+ when IR::GlobalDecl
43
+ validate_type!(node.element_type, context: "global #{node.name} element")
44
+ end
45
+ end
46
+
47
+ def validate_builtin!(node)
48
+ return unless Builtins.function?(node.name)
49
+
50
+ unless Builtins.supported_on_target?(node.name, target)
51
+ raise TargetCapabilityError, "Builtin #{node.name} is not supported on #{target.to_s.upcase}"
52
+ end
53
+
54
+ Builtins.explicit_types(node.name).each do |type|
55
+ validate_type!(type, context: "builtin #{node.name}")
56
+ end
57
+
58
+ validate_c_builtin_overload!(node) if target == :c
59
+ end
60
+
61
+ def validate_c_builtin_overload!(node)
62
+ vector_types = node.args.map(&:type).select { |type| Builtins.vector_type?(type) }
63
+ scalar_only = %i[sqrt abs sign floor ceil fract mod min max clamp step smoothstep]
64
+ if scalar_only.include?(node.name.to_sym) && !vector_types.empty?
65
+ raise TargetCapabilityError,
66
+ "Builtin #{node.name} does not support vector arguments on C"
67
+ end
68
+
69
+ if %i[length normalize dot distance].include?(node.name.to_sym) && vector_types.empty?
70
+ raise TargetCapabilityError, "Builtin #{node.name} requires vector arguments on C"
71
+ end
72
+
73
+ return unless %i[reflect refract].include?(node.name.to_sym)
74
+ return if node.args.first&.type == :vec3 && node.args[1]&.type == :vec3
75
+
76
+ raise TargetCapabilityError, "Builtin #{node.name} currently requires vec3 arguments on C"
77
+ end
78
+
79
+ def validate_type!(type, context:)
80
+ return if type.nil?
81
+
82
+ if TypeShapes.array?(type)
83
+ validate_type!(TypeShapes.element_type(type), context: "#{context} array element")
84
+ return
85
+ end
86
+
87
+ if type.is_a?(Array)
88
+ type.each { |element_type| validate_type!(element_type, context: "#{context} tuple element") }
89
+ return
90
+ end
91
+
92
+ if type.is_a?(IR::TupleType)
93
+ type.types.each { |element_type| validate_type!(element_type, context: "#{context} tuple element") }
94
+ return
95
+ end
96
+
97
+ return unless uniform_type_symbol?(type)
98
+
99
+ return if RLSL::UniformTypes.fetch(type).target_supported?(target)
100
+
101
+ raise TargetCapabilityError,
102
+ "Unsupported #{target.to_s.upcase} type #{type.inspect} in #{context}"
103
+ end
104
+
105
+ def uniform_type_symbol?(type)
106
+ type.is_a?(Symbol) && RLSL::UniformTypes.supported?(type)
107
+ end
108
+ end
109
+ end
110
+ end