Skip to content

Commit bc76954

Browse files
committed
feat(idl): add constraints to IDL
1 parent a74cd0f commit bc76954

File tree

7 files changed

+333
-0
lines changed

7 files changed

+333
-0
lines changed

tools/ruby-gems/idlc/lib/idlc.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# frozen_string_literal: true
66

77
require "pathname"
8+
require "sorbet-runtime"
89
require "treetop"
910

1011
require_relative "idlc/syntax_node"
@@ -44,6 +45,8 @@ class IdlParser < Treetop::Runtime::CompiledParser; end
4445
module Idl
4546
# the Idl compiler
4647
class Compiler
48+
extend T::Sig
49+
4750
attr_reader :parser
4851

4952
def initialize
@@ -283,5 +286,42 @@ def compile_expression(expression, symtab, pass_error: false)
283286

284287
ast
285288
end
289+
290+
sig { params(body: String, symtab: SymbolTable, pass_error: T::Boolean).returns(ConstraintBodyAst) }
291+
def compile_constraint(body, symtab, pass_error: false)
292+
m = @parser.parse(body, root: :constraint_body)
293+
if m.nil?
294+
raise SyntaxError, <<~MSG
295+
While parsing #{body}:#{@parser.failure_line}:#{@parser.failure_column}
296+
297+
#{@parser.failure_reason}
298+
MSG
299+
end
300+
301+
# fix up left recursion
302+
ast = m.to_ast
303+
ast.set_input_file("[CONSTRAINT]", 0)
304+
ast.freeze_tree(symtab)
305+
306+
begin
307+
ast.type_check(symtab)
308+
rescue AstNode::TypeError => e
309+
raise e if pass_error
310+
311+
warn "Compiling #{body}"
312+
warn e.what
313+
warn T.must(e.backtrace).to_s
314+
exit 1
315+
rescue AstNode::InternalError => e
316+
raise e if pass_error
317+
318+
warn "Compiling #{body}"
319+
warn e.what
320+
warn T.must(e.backtrace).to_s
321+
exit 1
322+
end
323+
324+
ast
325+
end
286326
end
287327
end

tools/ruby-gems/idlc/lib/idlc/ast.rb

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2993,6 +2993,123 @@ def type_check(_symtab)
29932993
end
29942994
end
29952995

2996+
class ImplicationExpressionSyntaxNode < SyntaxNode
2997+
sig { override.returns(ImplicationExpressionAst) }
2998+
def to_ast
2999+
ImplicationExpressionAst.new(
3000+
input, interval,
3001+
antecedent.to_ast, consequent.to_ast
3002+
)
3003+
end
3004+
end
3005+
3006+
class ImplicationExpressionAst < AstNode
3007+
sig {
3008+
params(
3009+
input: String,
3010+
interval: T::Range[Integer],
3011+
antecedent: RvalueAst,
3012+
consequent: RvalueAst
3013+
).void
3014+
}
3015+
def initialize(input, interval, antecedent, consequent)
3016+
super(input, interval, [antecedent, consequent])
3017+
end
3018+
3019+
sig { returns(RvalueAst) }
3020+
def antecedent = @children[0]
3021+
3022+
sig { returns(RvalueAst) }
3023+
def consequent = @children[1]
3024+
3025+
sig { override.params(symtab: SymbolTable).void }
3026+
def type_check(symtab)
3027+
antecedent.type_error "Antecedent must a boolean" unless antecedent.type(symtab).kind == :boolean
3028+
consequent.type_error "Consequent must a boolean" unless consequent.type(symtab).kind == :boolean
3029+
end
3030+
3031+
sig { params(symtab: SymbolTable).returns(T::Boolean) }
3032+
def satisfied?(symtab)
3033+
return true if antecedent.value(symtab) == false
3034+
consequent.value(symtab)
3035+
end
3036+
3037+
end
3038+
3039+
class ImplicationStatementSyntaxNode < SyntaxNode
3040+
sig { override.returns(ImplicationStatementAst) }
3041+
def to_ast
3042+
ImplicationStatementAst.new(input, interval, implication_expression.to_ast)
3043+
end
3044+
end
3045+
3046+
class ImplicationStatementAst < AstNode
3047+
sig {
3048+
params(
3049+
input: String,
3050+
interval: T::Range[Integer],
3051+
implication_expression: ImplicationExpressionAst
3052+
).void
3053+
}
3054+
def initialize(input, interval, implication_expression)
3055+
super(input, interval, [implication_expression])
3056+
end
3057+
3058+
sig { returns(ImplicationExpressionAst) }
3059+
def expression = @children[0]
3060+
3061+
sig { override.params(symtab: SymbolTable).void }
3062+
def type_check(symtab)
3063+
expression.type_check(symtab)
3064+
end
3065+
3066+
sig { params(symtab: SymbolTable).returns(T::Boolean) }
3067+
def satisfied?(symtab)
3068+
expression.satisfied?(symtab)
3069+
end
3070+
end
3071+
3072+
class ConstraintBodySyntaxNode < SyntaxNode
3073+
sig { override.returns(ConstraintBodyAst) }
3074+
def to_ast
3075+
stmts = []
3076+
elements.each do |e|
3077+
stmts << e.i.to_ast
3078+
end
3079+
ConstraintBodyAst.new(input, interval, stmts)
3080+
end
3081+
end
3082+
3083+
class ConstraintBodyAst < AstNode
3084+
sig {
3085+
params(
3086+
input: String,
3087+
interval: T::Range[Integer],
3088+
stmts: T::Array[T.any(ImplicationStatementAst, ForLoopAst)]
3089+
).void
3090+
}
3091+
def initialize(input, interval, stmts)
3092+
super(input, interval, stmts)
3093+
end
3094+
3095+
sig { returns(T::Array[T.any(ImplicationStatementAst, ForLoopAst)]) }
3096+
def stmts = T.cast(@children, T::Array[T.any(ImplicationStatementAst, ForLoopAst)])
3097+
3098+
sig { override.params(symtab: SymbolTable).void }
3099+
def type_check(symtab)
3100+
stmts.each do |stmt|
3101+
stmt.type_check(symtab)
3102+
end
3103+
end
3104+
3105+
sig { params(symtab: SymbolTable).returns(T::Boolean) }
3106+
def satisfied?(symtab)
3107+
stmts.all? do |stmt|
3108+
stmt.satisfied?(symtab)
3109+
end
3110+
end
3111+
end
3112+
29963113
class WidthRevealSyntaxNode < SyntaxNode
29973114
def to_ast
29983115
WidthRevealAst.new(input, interval, send(:expression).to_ast)
@@ -6429,6 +6546,23 @@ def type_check(symtab)
64296546
symtab.pop
64306547
end
64316548

6549+
sig { params(symtab: SymbolTable).returns(T::Boolean) }
6550+
def satisfied?(symtab)
6551+
symtab.push(self)
6552+
begin
6553+
init.execute(symtab)
6554+
while condition.value(symtab)
6555+
stmts.each do |s|
6556+
return false unless s.satisfied?(symtab)
6557+
end
6558+
update.execute(symtab)
6559+
end
6560+
return true
6561+
ensure
6562+
symtab.pop
6563+
end
6564+
end
6565+
64326566
# @!macro return_value
64336567
def return_value(symtab)
64346568
symtab.push(self)

tools/ruby-gems/idlc/lib/idlc/idl.treetop

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,24 @@ grammar Idl
393393
e:template_safe_p9_binary_expression space* '?' space* t:expression space* ':' space* f:expression <Idl::TernaryOperatorExpressionSyntaxNode>
394394
end
395395

396+
rule implication_expression
397+
antecedent:p9_binary_expression space* '->' space* consequent:p9_binary_expression <Idl::ImplicationExpressionSyntaxNode>
398+
end
399+
400+
rule implication_for_loop
401+
'for' space* '(' space* single_declaration_with_initialization space* ';' space* condition:expression space* ';' space* action:(assignment / post_inc / post_dec) space* ')' space* '{' space*
402+
stmts:(s:(implication_statement / implication_for_loop) space*)+
403+
'}' <Idl::ForLoopSyntaxNode>
404+
end
405+
406+
rule implication_statement
407+
implication_expression space* ';' <Idl::ImplicationStatementSyntaxNode>
408+
end
409+
410+
rule constraint_body
411+
(i:(implication_statement / implication_for_loop) space*)+ <Idl::ConstraintBodySyntaxNode>
412+
end
413+
396414
rule expression
397415
(
398416
ternary_expression
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2+
3+
# SPDX-License-Identifier: BSD-3-Clause-Clear
4+
5+
# c: constraint body, which should cause a type error
6+
7+
tests:
8+
- c: |
9+
true -> 1;
10+
- c: |
11+
1 -> false;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2+
3+
# SPDX-License-Identifier: BSD-3-Clause-Clear
4+
5+
# c: constraint body
6+
# r: result -- true if pass, false if not
7+
8+
tests:
9+
- c: |
10+
true -> true;
11+
r: true
12+
- c: |
13+
true -> false;
14+
r: false
15+
- c: |
16+
false -> true;
17+
r: true
18+
- c: |
19+
false -> false;
20+
r: true
21+
- c: |
22+
(false && true) -> false;
23+
r: true
24+
- c: |
25+
(false || true) -> false;
26+
r: false
27+
- c: |
28+
true -> (false && true);
29+
r: false
30+
- c: |
31+
true -> (false || true);
32+
r: true
33+
- c: |
34+
TRUE_PARAM -> TRUE_PARAM;
35+
p:
36+
TRUE_PARAM: true
37+
r: true
38+
- c: |
39+
for (U32 i = 0; i < 8; i++) {
40+
TRUE_ARRAY[i] -> TRUE_ARRAY[i];
41+
}
42+
p:
43+
TRUE_ARRAY: [true, true, true, true, true, true, true, true]
44+
r: true

tools/ruby-gems/idlc/test/run.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
22
# SPDX-License-Identifier: BSD-3-Clause-Clear
33

4+
# typed: false
45
# frozen_string_literal: true
56

67
require "simplecov"
@@ -14,4 +15,5 @@
1415
require "minitest/autorun"
1516

1617
require_relative "test_expressions"
18+
require_relative "test_constraints"
1719
require_relative "test_cli"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2+
# SPDX-License-Identifier: BSD-3-Clause-Clear
3+
4+
# typed: false
5+
# frozen_string_literal: true
6+
7+
require "minitest/autorun"
8+
9+
require "yaml"
10+
11+
require_relative "helpers"
12+
require "idlc"
13+
14+
$root ||= (Pathname.new(__FILE__) / ".." / ".." / ".." / "..").realpath
15+
16+
def to_idl_type(value)
17+
case value
18+
when Integer
19+
width = value.zero? ? 1 : value.bit_length
20+
Idl::Type.new(:bits, width:)
21+
when String
22+
Idl::Type.new(:string)
23+
when TrueClass, FalseClass
24+
Idl::Type.new(:boolean)
25+
when Array
26+
Idl::Type.new(:array, sub_type: to_idl_type(value[0]))
27+
else
28+
raise "Unexepected type"
29+
end
30+
end
31+
32+
class ConstraintTestFactory
33+
def self.create(klass_name, yaml_path)
34+
raise ArgumentError, "klass_name must be a String" unless klass_name.is_a?(String) && klass_name.size > 0
35+
raise ArgumentError, "klass_name must be uppercase" unless klass_name[0] == klass_name[0].upcase
36+
37+
# Test IDL constraints
38+
Object.const_set(klass_name,
39+
Class.new(Minitest::Test) do
40+
include TestMixin
41+
make_my_diffs_pretty!
42+
43+
def setup
44+
@symtab = Idl::SymbolTable.new
45+
@compiler = Idl::Compiler.new
46+
end
47+
48+
test_yaml = YAML.load(File.read("#{Kernel.__dir__}/#{yaml_path}"))
49+
test_yaml["tests"].each_with_index do |test, i|
50+
define_method "test_#{i}" do
51+
if test.key?("p")
52+
@symtab.push(nil)
53+
test["p"].each do |name, value|
54+
@symtab.add!(name, Idl::Var.new(name, to_idl_type(value), value))
55+
end
56+
end
57+
constraint_ast = nil
58+
if test["r"].nil?
59+
assert_raises Idl::AstNode::TypeError do
60+
@compiler.compile_constraint(test["c"], @symtab, pass_error: true)
61+
end
62+
else
63+
out, err = capture_io do
64+
constraint_ast = @compiler.compile_constraint(test["c"], @symtab)
65+
end
66+
67+
if test["r"]
68+
assert constraint_ast.satisfied?(@symtab)
69+
else
70+
refute constraint_ast.satisfied?(@symtab)
71+
end
72+
end
73+
74+
@symtab.pop if test.key?("p")
75+
end
76+
end
77+
end
78+
)
79+
end
80+
end
81+
82+
# now list all the YAML files that specify constraints to test
83+
ConstraintTestFactory.create("Constraints", "idl/constraints.yaml")
84+
ConstraintTestFactory.create("BadConstraints", "idl/constraint_errors.yaml")

0 commit comments

Comments
 (0)