Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/docs-resources
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume this shouldn't be included.

176 changes: 172 additions & 4 deletions tools/ruby-gems/idlc/lib/idlc/ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6322,6 +6322,53 @@ class StatementAst < AstNode
sig { override.params(symtab: SymbolTable).returns(T::Boolean) }
def const_eval?(symtab) = action.const_eval?(symtab)

# Check if this statement guarantees a return
# @param symtab [SymbolTable] Symbol table for context
# @return [Boolean] true if it guarantees a return, false otherwise
def all_paths_return?(symtab)
if action.is_a?(ReturnStatementAst) || action.is_a?(ReturnExpressionAst)
return true
end
# Treat unreachable() as a guaranteed return path
if action.is_a?(FunctionCallExpressionAst) && action.name == "unreachable"
return true
end
# Treat unpredictable() as a guaranteed return path
if action.is_a?(FunctionCallExpressionAst) && action.name == "unpredictable"
return true
end
# Treat abort_current_instruction() as a guaranteed return path
if action.is_a?(FunctionCallExpressionAst) && action.name == "abort_current_instruction"
return true
end

# Check if the function call always terminates (recursive check via callee's body)
if action.is_a?(FunctionCallExpressionAst)
ft = symtab.get(action.name)
if ft.is_a?(FunctionType) && ft.func_def_ast.always_terminates?(symtab)
return true
end
end

# treat assert with a known false condition as a guaranteed return path
if action.is_a?(FunctionCallExpressionAst) && action.name == "assert"
fc = T.cast(action, FunctionCallExpressionAst)
if fc.args.size == 0
type_error "assert is missing a condition"
end
cond = fc.args.fetch(0)
value_try do
if cond.value(symtab) == false
return true
end
end
end
if action.is_a?(IfAst)
return action.all_paths_return?(symtab)
end
false
end

def action = @children[0]

def initialize(input, interval, action)
Expand Down Expand Up @@ -7940,6 +7987,40 @@ def statements = @children

def stmts = @children

# Check if all execution paths in this function body end with a return statement
# @param symtab [SymbolTable] Symbol table for context
# @return [Boolean] true if all paths return, false otherwise
def all_paths_return?(symtab)
# Check each statement in order
stmts.each do |stmt|
# If we find a return statement, all paths from here return
if stmt.is_a?(ReturnStatementAst)
return true
end

# Treat unreachable() as a guaranteed return path
if stmt.is_a?(StatementAst) && stmt.action.is_a?(FunctionCallExpressionAst) && stmt.action.name == "unreachable"
return true
end

# If we find an if statement, check if all its paths return
if stmt.is_a?(IfAst) && stmt.all_paths_return?(symtab)
return true
end

# A StatementAst might contain an IfAst or a return
if stmt.is_a?(StatementAst) && stmt.all_paths_return?(symtab)
return true
end

# For loops can't guarantee a return (might not execute)
# Other statements don't affect control flow for returns
end

# If we get here, no statement guaranteed a return
false
end

# @!macro type_check
def type_check(symtab, strict:)
internal_error "Function bodies should be at global + 1 scope (at #{symtab.levels})" unless symtab.levels == 2
Expand Down Expand Up @@ -8138,6 +8219,11 @@ class FunctionDefAst < AstNode

attr_reader :return_type_nodes

class Memo < T::Struct
prop :always_terminates, T::Hash[SymbolTable, T::Boolean], default: {}
prop :computing_always_terminates, T::Boolean, default: false
end

def <=>(other)
return nil unless other.is_a?(FunctionDefAst)

Expand Down Expand Up @@ -8174,6 +8260,7 @@ def initialize(input, interval, name, targs, return_types, arguments, desc, type
@generated = type == :generated
@external = type == :external

@memo = Memo.new
@cached_return_type = {}
@reachable_functions_cache ||= {}
end
Expand Down Expand Up @@ -8475,6 +8562,35 @@ def type_check_body(symtab, strict:)
return if @body.nil?

@body.type_check(symtab, strict:)

# Check that all paths return if the function return type is non-void
rtype = return_type(symtab)

# TODO: uncomment this once we merge with removal of template functions
# if rtype.kind != :void && rtype.kind != :tuple && !@body.all_paths_return?(symtab)
# type_error "Function '#{name}' has a non-void return type but not all execution paths return a value"
# end
end

# Returns true if every execution path in this function ends in a terminal
# (return, unreachable, unpredictable, raise_precise, or a call to another
# always-terminating function). Memoized after first calculation.
# @param symtab [SymbolTable] Symbol table for context
# @return [Boolean]
def always_terminates?(symtab)
return @memo.always_terminates[symtab] if @memo.always_terminates.key?(symtab)

# Guard against infinite recursion (mutual recursion between functions)
return false if @memo.computing_always_terminates

@memo.computing_always_terminates = true
begin
result = !builtin? && !generated? && !external? && !@body.nil? && @body.all_paths_return?(symtab)
@memo.always_terminates[symtab] = result
ensure
@memo.computing_always_terminates = false
end
@memo.always_terminates[symtab]
end

def body
Expand Down Expand Up @@ -8760,6 +8876,13 @@ def execute_unknown(symtab)
nil
end

# A for loop cannot guarantee a return because it might not execute
# @param symtab [SymbolTable] Symbol table for context
# @return [Boolean] always false
def all_paths_return?(symtab)
false
end

# @!macro to_idl
sig { override.returns(String) }
def to_idl
Expand Down Expand Up @@ -8818,6 +8941,26 @@ def initialize(input, interval, body_stmts)
end
end

# Check if all execution paths in this block end with a return statement
# @param symtab [SymbolTable] Symbol table for context
# @return [Boolean] true if all paths return, false otherwise
def all_paths_return?(symtab)
stmts.each do |stmt|
if stmt.is_a?(ReturnStatementAst)
return true
end

if stmt.is_a?(IfAst) && stmt.all_paths_return?(symtab)
return true
end

if stmt.is_a?(StatementAst) && stmt.all_paths_return?(symtab)
return true
end
end
false
end

# @!macro type_check
def type_check(symtab, strict:)
symtab.push(self)
Expand Down Expand Up @@ -8959,6 +9102,13 @@ def cond = T.cast(@children.fetch(0), RvalueAst)
sig { returns(IfBodyAst) }
def body = T.cast(@children.fetch(1), IfBodyAst)

# Check if all execution paths in this block end with a return statement
# @param symtab [SymbolTable] Symbol table for context
# @return [Boolean] true if all paths return, false otherwise
def all_paths_return?(symtab)
body.all_paths_return?(symtab)
end

sig { params(input: T.nilable(String), interval: T.nilable(T::Range[Integer]), body_interval: T.nilable(T::Range[Integer]), cond: RvalueAst, body_stmts: T::Array[StatementAst]).void }
def initialize(input, interval, body_interval, cond, body_stmts)
body = IfBodyAst.new(input, body_interval, body_stmts)
Expand Down Expand Up @@ -9106,6 +9256,24 @@ def initialize(input, interval, if_cond, if_body, elseifs, final_else_body)
super(input, interval, children_nodes)
end

# Check if all execution paths in this if structure end with a return statement
# For an if statement to guarantee a return, the if body, all else ifs, AND the final else
# must all guarantee a return. Also, there MUST be a final else.
# @param symtab [SymbolTable] Symbol table for context
# @return [Boolean] true if all paths return, false otherwise
def all_paths_return?(symtab)
# If there is no final else block, we can't guarantee a return
# because the condition might be false and we just fall through
if final_else_body.stmts.empty?
return false
end

# All branches must guarantee a return
if_body.all_paths_return?(symtab) &&
elseifs.all? { |eif| eif.all_paths_return?(symtab) } &&
final_else_body.all_paths_return?(symtab)
end

# @!macro type_check
def type_check(symtab, strict:)
level = symtab.levels
Expand Down Expand Up @@ -9523,7 +9691,7 @@ class CsrReadExpressionAst < AstNode
include Rvalue

class Memo < T::Struct
prop :csr_obj, T.nilable(Csr)
prop :csr_obj, T::Hash[SymbolTable, Csr]
prop :type, T.nilable(Type)
end

Expand All @@ -9536,7 +9704,7 @@ def initialize(input, interval, csr_name)
super(input, interval, [])

@csr_name = csr_name
@memo = Memo.new
@memo = Memo.new(csr_obj: {})
end

def freeze_tree(symtab)
Expand All @@ -9549,15 +9717,15 @@ def freeze_tree(symtab)
end

# @!macro type
def type(symtab) = @memo.type ||= CsrType.new(csr_def(symtab))
def type(symtab) = @memo.type ||= CsrType.new(csr_def(symtab)).freeze

# @!macro type_check
def type_check(symtab, strict:)
type_error "CSR '#{@csr_name}' is not defined" unless symtab.csr?(@csr_name)
end

def csr_def(symtab)
@memo.csr_obj ||= symtab.csr(@csr_name)
@memo.csr_obj[symtab] ||= symtab.csr(@csr_name)
end

def csr_known?(symtab)
Expand Down
7 changes: 7 additions & 0 deletions tools/ruby-gems/idlc/lib/idlc/passes/prune.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ def prune(symtab, forced_type: nil)
# make sure that any variable assigned within the loop is considered unknown
stmts.each { |stmt| stmt.nullify_assignments(symtab) }

# Nullify any outer-scope variable assigned in the loop body before pruning,
# since we don't know how many iterations ran (or if any ran at all).
# This must happen before pruning the body so that optimizations (e.g.,
# "0 | x => x") don't incorrectly fire using the pre-loop value of a
# variable that is updated inside the loop.
stmts.each { |stmt| stmt.nullify_assignments(symtab) }

begin
new_loop =
ForLoopAst.new(
Expand Down
19 changes: 18 additions & 1 deletion tools/ruby-gems/udb-gen/sorbet/rbi/gems/idlc@0.1.0.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading