Class: Autotype::FixedPointInferencer

Inherits:
Object
  • Object
show all
Defined in:
lib/autotype/engine.rb

Constant Summary collapse

MAX_ITERATIONS =
30
PREDICATES =
%i[
  == != < <= > >= all? any? blank? connected? empty? eql? include?
  is_a? key? member? nil? present? respond_to? start_with? end_with? zero?
].to_set.freeze
INTEGER_RESULTS =
%i[count length size bytesize].to_set.freeze
STRING_RESULTS =
%i[
  strip lstrip rstrip chomp chop squeeze tr downcase upcase capitalize
  swapcase gsub sub join to_json encode truncate squish
  strip! chomp! gsub! sub! downcase! upcase! squish!
].to_set.freeze
STRING_LIST_RESULTS =
%i[split lines chars].to_set.freeze
NORETURN =
Named.new("noreturn")

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(methods, constants: {}, includes: {}, metadata: InferenceMetadata.empty) ⇒ FixedPointInferencer

Returns a new instance of FixedPointInferencer.



1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
# File 'lib/autotype/engine.rb', line 1102

def initialize(methods, constants: {}, includes: {}, metadata: InferenceMetadata.empty)
  # `initialize` is only reachable through `new`, which returns the
  # instance, so its meaningful type is the owner class.
  @methods = methods.map do |method|
    next method unless method.kind == :instance && method.method_name == :initialize

    method.with(result: Named.new(method.owner))
  end
  @index = @methods.to_h { |method| [[method.owner, method.kind, method.method_name], method] }
  @superclasses = methods.filter_map do |method|
    [method.owner, method.superclass] if method.superclass
  end.to_h
  @constants = constants
  @includes = resolve_includes(includes)
  @metadata = 
  @duck_index = Hash.new { |hash, key| hash[key] = [] }
  @methods.each do |method|
    next unless method.kind == :instance && method.method_name != :initialize

    @duck_index[method.method_name] << method
  end
  @substitutions = {}
  @call_mappings = {}
  @stable_variables = {}
  @fresh_scope = Object.new
  @fresh_id = 0
  @iterations = 0
  @converged = false
end

Instance Attribute Details

#iterationsObject (readonly)

Returns the value of attribute iterations.



1100
1101
1102
# File 'lib/autotype/engine.rb', line 1100

def iterations
  @iterations
end

Instance Method Details

#apply_case_branch_narrowing!Object



1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
# File 'lib/autotype/engine.rb', line 1226

def apply_case_branch_narrowing!
  @methods.each do |method|
    method.case_narrowings.each do |name, types|
      parameter = method.parameters.find { |candidate| candidate.name == name }
      next unless parameter

      merged = types.reduce { |left, right| merge_union(left, right) }
      unify(parameter.type, merged) if merged
    end
  end
end

#apply_config_hash_wiring!Object



1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
# File 'lib/autotype/engine.rb', line 1334

def apply_config_hash_wiring!
  wiring = @metadata.config_hash
  @metadata.config_options.each do |owner, options|
    next if options.empty?

    init = @index[[owner, :instance, :initialize]]
    next unless init

    config_param = init.parameters.find { |parameter| parameter.name == wiring.init_param }
    next unless config_param

    value_type = options.values.uniq
    value_type = value_type.length == 1 ? value_type.first : Union.new(value_type)
    unify(config_param.type, Generic.new("Hash", [Named.new("Symbol"), value_type]))
  end

  @methods.each do |method|
    options = @metadata.config_options[method.owner]
    next unless options

    method.capabilities.each do |capability|
      next unless %i[fetch [] dig].include?(capability.message)
      next unless config_hash_receiver?(method, capability.receiver, wiring)

      key = literal_symbol_key(capability.arguments.first)
      next unless key

      option_type = options[key]
      unify(capability.result, option_type) if option_type
    end
  end
end

#apply_core_member_type_conventions!Object



1177
1178
1179
1180
1181
1182
1183
1184
1185
# File 'lib/autotype/engine.rb', line 1177

def apply_core_member_type_conventions!
  CORE_MEMBER_NAME_TYPES.each do |member, type_name|
    type = Named.new(type_name)
    @duck_index[member].each do |accessor|
      unify(accessor.result, type)
      accessor.ivars.each_value { |ivar| unify(ivar, type) }
    end
  end
end

#apply_declared_annotations!Object



1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
# File 'lib/autotype/engine.rb', line 1166

def apply_declared_annotations!
  apply_declared_member_types!
  apply_factory_param_bindings!
  apply_case_branch_narrowing!
  apply_core_member_type_conventions!
  apply_initialize_param_bindings!
  apply_port_handler_wiring! if @metadata.port_wiring?
  apply_output_emit_wiring! if @metadata.output_emit?
  apply_config_hash_wiring! if @metadata.config_hash?
end

#apply_declared_member_types!Object



1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
# File 'lib/autotype/engine.rb', line 1198

def apply_declared_member_types!
  @metadata.member_types.each do |owner, fields|
    fields.each do |member, type|
      accessor = @index[[owner, :instance, member]]
      next unless accessor

      unify(accessor.result, type)
      accessor.ivars.each_value { |ivar| unify(ivar, type) }
    end
  end
end

#apply_factory_param_bindings!Object



1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
# File 'lib/autotype/engine.rb', line 1210

def apply_factory_param_bindings!
  @metadata.member_types.each do |owner, fields|
    %i[create new].each do |method_name|
      factory = @index[[owner, :singleton, method_name]]
      next unless factory

      factory.parameters.each do |parameter|
        next unless %i[keyword optional_keyword].include?(parameter.kind)

        type = fields[parameter.name]
        unify(parameter.type, type) if type
      end
    end
  end
end

#apply_initialize_param_bindings!Object



1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
# File 'lib/autotype/engine.rb', line 1187

def apply_initialize_param_bindings!
  @methods.each do |method|
    next unless method.kind == :instance && method.method_name == :initialize

    method.parameters.each do |parameter|
      ivar = method.ivars[:"@#{parameter.name}"]
      unify(parameter.type, ivar) if ivar
    end
  end
end

#apply_output_emit_wiring!Object



1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
# File 'lib/autotype/engine.rb', line 1272

def apply_output_emit_wiring!
  wiring = @metadata.output_emit
  @metadata.ports.each do |owner, ports|
    outputs = ports[:outputs]
    next if outputs.empty?

    @methods.select { |method| method.owner == owner }.each do |method|
      method.capabilities.each do |capability|
        next unless capability.message == wiring.message
        next unless capability.receiver == method.self_type

        port_key = literal_symbol_key(capability.keywords[wiring.port_keyword])
        next unless port_key

        output_type = outputs[port_key]
        next unless output_type

        payload = capability.arguments[wiring.argument_index]
        unify(payload, output_type) if payload
      end
    end
  end
end

#apply_port_assignments!Object



1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
# File 'lib/autotype/engine.rb', line 1316

def apply_port_assignments!
  @methods.each do |method|
    next if method.port_assignments.empty?

    ports = @metadata.ports.dig(method.owner, :inputs) || {}
    method.port_assignments.each do |assignment|
      port_type = ports[assignment.fetch(:port)]
      next unless port_type

      param = method.parameters.find { |parameter| parameter.name == assignment.fetch(:param) }
      unify(param.type, port_type) if param

      ivar = method.ivars[assignment.fetch(:ivar)]
      unify(ivar, port_type) if assignment.fetch(:ivar) && ivar
    end
  end
end

#apply_port_handler_wiring!Object



1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
# File 'lib/autotype/engine.rb', line 1238

def apply_port_handler_wiring!
  wiring = @metadata.port_wiring
  @metadata.ports.each do |owner, ports|
    inputs = ports[:inputs]
    next if inputs.empty?

    handler = @index[[owner, :instance, wiring.handler_method]]
    next unless handler

    entity_param = handler_entity_param(handler, wiring.entity_param)
    dispatch_param = handler.parameters.find do |parameter|
      parameter.name == wiring.dispatch_param &&
        %i[keyword optional_keyword].include?(parameter.kind)
    end

    if entity_param
      entity_type = port_type_for_param(entity_param.name, inputs)
      unify(entity_param.type, entity_type) if entity_type
    end

    unify(dispatch_param.type, wiring.dispatch_param_type) if dispatch_param
  end
  apply_port_assignments!
end

#config_hash_receiver?(method, receiver, wiring) ⇒ Boolean

Returns:

  • (Boolean)


1367
1368
1369
1370
1371
1372
# File 'lib/autotype/engine.rb', line 1367

def config_hash_receiver?(method, receiver, wiring)
  terminal = dereference(resolve(receiver))
  return true if terminal == Named.new("Hash")

  method.ivars.key?(wiring.ivar) && dereference(method.ivars[wiring.ivar]) == terminal
end

#converged?Boolean

Returns:

  • (Boolean)


1164
# File 'lib/autotype/engine.rb', line 1164

def converged? = @converged

#core_member_type_for_key(key_arg) ⇒ Object



1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
# File 'lib/autotype/engine.rb', line 1418

def core_member_type_for_key(key_arg)
  name = literal_key_name(key_arg)&.to_sym
  return unless name

  if (type_name = CORE_MEMBER_NAME_TYPES[name])
    return Named.new(type_name)
  end

  @metadata.member_type_hints[name]
end

#entity_hash_type(owner) ⇒ Object



1504
1505
1506
1507
1508
1509
1510
1511
1512
# File 'lib/autotype/engine.rb', line 1504

def entity_hash_type(owner)
  fields = @metadata.member_types[owner]
  if fields.nil? || fields.empty?
    return Generic.new("Hash", [Named.new("String"), Named.new("Object")])
  end

  value_type = fields.values.reduce { |left, right| merge_union(left, right) }
  Generic.new("Hash", [Named.new("String"), value_type])
end

#handler_entity_param(handler, selector) ⇒ Object



1263
1264
1265
1266
1267
1268
1269
1270
# File 'lib/autotype/engine.rb', line 1263

def handler_entity_param(handler, selector)
  case selector
  when :first_required
    handler.parameters.find { |parameter| parameter.kind == :required }
  when Symbol
    handler.parameters.find { |parameter| parameter.name == selector }
  end
end

#hash_dig_type(hash_type, keys) ⇒ Object



1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
# File 'lib/autotype/engine.rb', line 1466

def hash_dig_type(hash_type, keys)
  current = hash_type
  keys.each do |key|
    return nil unless hash?(current)

    if literal_hash_key?(current, key) || hash_key_type_compatible?(current, key)
      current = current.arguments[1]
    else
      current = current.arguments[1]
    end
  end
  current
end

#hash_key_type_compatible?(hash_type, key_arg) ⇒ Boolean

Returns:

  • (Boolean)


1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
# File 'lib/autotype/engine.rb', line 1453

def hash_key_type_compatible?(hash_type, key_arg)
  case hash_type
  when Generic
    return false unless hash?(hash_type)

    dereference(hash_type.arguments[0]) == dereference(key_arg)
  when Union
    hash_type.members.any? { |member| hash_key_type_compatible?(member, key_arg) }
  else
    false
  end
end

#hash_new_block_value_type(block) ⇒ Object



1429
1430
1431
1432
1433
1434
1435
# File 'lib/autotype/engine.rb', line 1429

def hash_new_block_value_type(block)
  resolved = resolve(block.result)
  return resolved if hash?(resolved) || array?(resolved)
  return resolved if ground?(resolved) && !resolved.is_a?(Named)

  nil
end

#literal_hash_key?(hash_type, key) ⇒ Boolean

Returns:

  • (Boolean)


1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
# File 'lib/autotype/engine.rb', line 1380

def literal_hash_key?(hash_type, key)
  literal_key = literal_symbol_key(key) || literal_integer_key(key)
  return false unless literal_key

  case hash_type
  when Generic
    return false unless hash?(hash_type)

    key_type = dereference(hash_type.arguments[0])
    case key_type
    when Named then key_type.name == "Symbol" || key_type.name == "Integer"
    else false
    end
  when Union
    hash_type.members.any? { |member| literal_hash_key?(member, key) }
  else
    false
  end
end

#literal_integer_key(key) ⇒ Object



1400
1401
1402
1403
1404
1405
1406
1407
# File 'lib/autotype/engine.rb', line 1400

def literal_integer_key(key)
  case key
  when Named
    return key.name.to_i if key.name.match?(/\A-?\d+\z/)

    nil
  end
end

#literal_key_name(key) ⇒ Object



1409
1410
1411
1412
1413
1414
1415
1416
# File 'lib/autotype/engine.rb', line 1409

def literal_key_name(key)
  case key
  when Named
    return key.name unless RESERVED_TYPE_NAMES.include?(key.name)

    nil
  end
end

#literal_symbol_key(key) ⇒ Object



1374
1375
1376
1377
1378
# File 'lib/autotype/engine.rb', line 1374

def literal_symbol_key(key)
  case key
  when Named then key.name.to_sym
  end
end

#param_matches_port?(param_name, port_name) ⇒ Boolean

Returns:

  • (Boolean)


1306
1307
1308
1309
1310
1311
1312
1313
1314
# File 'lib/autotype/engine.rb', line 1306

def param_matches_port?(param_name, port_name)
  param = param_name.to_s
  port = port_name.to_s
  return true if param == port
  return true if port == "#{param}s" || port == "#{param}es"
  return true if port.end_with?("_#{param}") || port.start_with?("#{param}_")

  false
end

#port_type_for_param(param_name, inputs) ⇒ Object



1296
1297
1298
1299
1300
1301
1302
1303
1304
# File 'lib/autotype/engine.rb', line 1296

def port_type_for_param(param_name, inputs)
  return inputs.values.first if inputs.length == 1

  matched = inputs.find { |port_name, _| param_matches_port?(param_name, port_name) }
  return matched.last if matched

  members = inputs.values.uniq
  members.length == 1 ? members.first : Union.new(members)
end

#runObject



1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
# File 'lib/autotype/engine.rb', line 1132

def run
  apply_declared_annotations!
  anchor_constructor_definitions
  solve_to_fixed_point

  # Newly grounded parameters make more receivers concrete, so another
  # solving round can resolve targets that failed before. The second
  # round lets values flow through one level of indirection (caller ->
  # factory -> constructor).
  2.times do
    flow_call_arguments!
    flow_structural_arguments!
    solve_to_fixed_point
  end

  apply_late_send_defaults!
  apply_definition_conventions!
  narrow_residual_returns!
  unify_instance_variables
  solve_to_fixed_point
  apply_intra_method_param_flow!
  apply_chained_send_narrowing!
  apply_block_argument_callee_flow!
  flow_call_arguments!
  solve_to_fixed_point
  reflow_callee_returns!
  solve_to_fixed_point
  apply_mutator_return_nil!
  apply_forced_return_conventions!
  @methods.map { |method| resolved_method(method) }
end

#structured_owner?(owner) ⇒ Boolean

Returns:

  • (Boolean)


1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
# File 'lib/autotype/engine.rb', line 1487

def structured_owner?(owner)
  return true if @metadata.structured_type?(owner)

  init = @index[[owner, :instance, :initialize]]
  return false unless init

  init.parameters.any? do |parameter|
    %i[keyword optional_keyword].include?(parameter.kind) &&
      (parameter.name == :id || @metadata.member_type_hints.key?(parameter.name))
  end
end

#structured_owner_name(type) ⇒ Object



1499
1500
1501
1502
# File 'lib/autotype/engine.rb', line 1499

def structured_owner_name(type)
  terminal = dereference(type)
  terminal.is_a?(Named) && structured_owner?(terminal.name) ? terminal.name : nil
end

#structured_type?(type, _method) ⇒ Boolean

Returns:

  • (Boolean)


1480
1481
1482
1483
1484
1485
# File 'lib/autotype/engine.rb', line 1480

def structured_type?(type, _method)
  terminal = dereference(type)
  return false unless terminal.is_a?(Named)

  structured_owner?(terminal.name)
end

#unify_hash_key_argument!(hash_type, key_arg) ⇒ Object



1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
# File 'lib/autotype/engine.rb', line 1437

def unify_hash_key_argument!(hash_type, key_arg)
  return unless hash?(hash_type)

  terminal = dereference(key_arg)
  case terminal
  when Named
    if terminal.name == "Integer" || terminal.name == "Symbol"
      unify(hash_type.arguments[0], terminal)
    elsif terminal.name.match?(/\A-?\d+\z/)
      unify(hash_type.arguments[0], Named.new("Integer"))
    elsif literal_symbol_key(terminal)
      unify(hash_type.arguments[0], Named.new("Symbol"))
    end
  end
end