Class: TypeProf::Core::MethodDeclBox

Inherits:
Box
  • Object
show all
Defined in:
lib/typeprof/core/graph/box.rb

Instance Attribute Summary collapse

Attributes inherited from Box

#changes, #destroyed

Instance Method Summary collapse

Methods inherited from Box

#add_symbol_proc_call_box, #destroy_symbol_proc_call_boxes, #on_type_added, #on_type_removed, #reuse, #run, #run0, #to_s

Constructor Details

#initialize(node, genv, cpath, singleton, mid, method_types, overloading) ⇒ MethodDeclBox

Returns a new instance of MethodDeclBox.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/typeprof/core/graph/box.rb', line 263

def initialize(node, genv, cpath, singleton, mid, method_types, overloading)
  super(node)
  @cpath = cpath
  @singleton = singleton
  @mid = mid
  @method_types = method_types
  @overloading = overloading
  @ret = Source.new

  me = genv.resolve_method(@cpath, @singleton, @mid)
  me.add_decl(self)
  me.add_run_all_method_call_boxes(genv)
  me.add_run_all_mdefs(genv)
end

Instance Attribute Details

#cpathObject (readonly)

Returns the value of attribute cpath.



280
281
282
# File 'lib/typeprof/core/graph/box.rb', line 280

def cpath
  @cpath
end

#method_typesObject (readonly)

Returns the value of attribute method_types.



280
281
282
# File 'lib/typeprof/core/graph/box.rb', line 280

def method_types
  @method_types
end

#midObject (readonly)

Returns the value of attribute mid.



280
281
282
# File 'lib/typeprof/core/graph/box.rb', line 280

def mid
  @mid
end

#nodeObject

Returns the value of attribute node.



278
279
280
# File 'lib/typeprof/core/graph/box.rb', line 278

def node
  @node
end

#overloadingObject (readonly)

Returns the value of attribute overloading.



280
281
282
# File 'lib/typeprof/core/graph/box.rb', line 280

def overloading
  @overloading
end

#retObject (readonly)

Returns the value of attribute ret.



280
281
282
# File 'lib/typeprof/core/graph/box.rb', line 280

def ret
  @ret
end

#singletonObject (readonly)

Returns the value of attribute singleton.



280
281
282
# File 'lib/typeprof/core/graph/box.rb', line 280

def singleton
  @singleton
end

Instance Method Details

#destroy(genv) ⇒ Object



282
283
284
285
286
287
# File 'lib/typeprof/core/graph/box.rb', line 282

def destroy(genv)
  me = genv.resolve_method(@cpath, @singleton, @mid)
  me.remove_decl(self)
  me.add_run_all_method_call_boxes(genv)
  destroy_symbol_proc_call_boxes(genv)
end

#keyword_arg_typecheck?(genv, changes, keywords_vtx, key, expected_ty, param_map) ⇒ Boolean

Typecheck a single keyword argument value against the expected type by directly inspecting the pre-existing value vertices in the keywords vertex's types (Record, Hash, Instance).

Returns:

  • (Boolean)


542
543
544
545
546
547
548
549
550
551
552
553
# File 'lib/typeprof/core/graph/box.rb', line 542

def keyword_arg_typecheck?(genv, changes, keywords_vtx, key, expected_ty, param_map)
  keywords_vtx.each_type do |kw_ty|
    val_vtx = case kw_ty
    when Type::Hash then kw_ty.get_value(key)
    when Type::Record then kw_ty.get_value(key)
    when Type::Instance then kw_ty.mod == genv.mod_hash ? kw_ty.args[1] : nil
    else nil
    end
    return false if val_vtx && !expected_ty.typecheck(genv, changes, val_vtx, param_map)
  end
  true
end

#match_arguments?(genv, changes, param_map, a_args, method_type) ⇒ Boolean

Returns:

  • (Boolean)


289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/typeprof/core/graph/box.rb', line 289

def match_arguments?(genv, changes, param_map, a_args, method_type)
  # TODO: handle a tuple as a splat argument?
  if a_args.splat_flags.any?
    return false unless method_type.rest_positionals
    method_type.req_positionals.size.times do |i|
      return false if a_args.splat_flags[i]
    end
    method_type.post_positionals.size.times do |i|
      return false if a_args.splat_flags[-i - 1]
    end
  else
    actual = a_args.positionals.size
    required_formal = method_type.req_positionals.size + method_type.post_positionals.size
    if actual < required_formal
      # too few actual arguments
      return false
    end
    if !method_type.rest_positionals && actual > required_formal + method_type.opt_positionals.size
      # too many actual arguments
      return false
    end
  end

  method_type.req_positionals.each_with_index do |ty, i|
    return false unless ty.typecheck(genv, changes, a_args.positionals[i], param_map)
  end
  method_type.post_positionals.each_with_index do |ty, i|
    i -= method_type.post_positionals.size
    return false unless ty.typecheck(genv, changes, a_args.positionals[i], param_map)
  end

  start_rest = method_type.req_positionals.size
  end_rest = a_args.positionals.size - method_type.post_positionals.size

  i = 0
  while i < method_type.opt_positionals.size && start_rest < end_rest
    break if a_args.splat_flags[start_rest]
    return false unless method_type.opt_positionals[i].typecheck(genv, changes, a_args.positionals[start_rest], param_map)
    i += 1
    start_rest += 1
  end

  if start_rest < end_rest
    vtxs = a_args.get_rest_args(genv, changes, start_rest, end_rest)
    while i < method_type.opt_positionals.size
      ty = method_type.opt_positionals[i]
      return false if vtxs.any? {|vtx| !ty.typecheck(genv, changes, vtx, param_map) }
      i += 1
    end
    if method_type.rest_positionals
      return false if vtxs.any? {|vtx| !method_type.rest_positionals.typecheck(genv, changes, vtx, param_map) }
    end
  end

  # Check keyword arguments by inspecting the keywords vertex types
  # directly. We avoid get_keyword_arg here because it creates a fresh
  # Vertex each call, which would destabilize the change-set edges and
  # cause oscillation when match_arguments? runs on every box re-eval.
  if a_args.keywords
    method_type.req_keyword_keys.zip(method_type.req_keyword_values) do |key, ty|
      return false unless keyword_arg_typecheck?(genv, changes, a_args.keywords, key, ty, param_map)
    end
    method_type.opt_keyword_keys.zip(method_type.opt_keyword_values) do |key, ty|
      return false unless keyword_arg_typecheck?(genv, changes, a_args.keywords, key, ty, param_map)
    end
    if method_type.rest_keywords
      return false unless rest_keyword_args_typecheck?(genv, changes, a_args.keywords, method_type, param_map)
    end
  end

  return true
end

#resolve_overload(changes, genv, method_type, node, param_map, a_args, ret, force) ⇒ Object



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/typeprof/core/graph/box.rb', line 362

def resolve_overload(changes, genv, method_type, node, param_map, a_args, ret, force)
  param_map0 = param_map.dup
  if method_type.type_params
    method_type.type_params.zip(yield(method_type)) do |(var, _default_ty), vtx|
      param_map0[var] = vtx # TODO: default_ty?
    end
  end

  unless match_arguments?(genv, changes, param_map0, a_args, method_type)
    if force
      meth = node.mid_code_range ? :mid_code_range : :code_range
      changes.add_diagnostic(meth, "wrong type of arguments") # XXX: more friendly and fine-grained error message
    end
    return false
  end

  rbs_blk = method_type.block
  if method_type.block_required && !a_args.block
    if force
      meth = node.mid_code_range ? :mid_code_range : :code_range
      changes.add_diagnostic(meth, "block is expected") # XXX: more friendly error message
    end
    return false
  end
  if !rbs_blk && a_args.block
    if force
      meth = node.mid_code_range ? :mid_code_range : :code_range
      changes.add_diagnostic(meth, "block is not expected") # XXX: more friendly error message
    end
    return false
  end

  if rbs_blk && a_args.block
    # rbs_blk_func.optional_keywords, ...
    blk_a_args = rbs_blk.req_positionals.map do |blk_a_arg|
      blk_a_arg.covariant_vertex(genv, changes, param_map0)
    end
    a_args.block.each_type do |ty|
      case ty
      when Type::Proc
        ty.block.accept_args(genv, changes, blk_a_args)

        if ty.block.is_a?(Block)
          ty.block.next_boxes.each do |next_box|
            unless rbs_blk.return_type.typecheck(genv, changes, next_box.a_ret, param_map0)
              next_box.wrong_return_type(rbs_blk.return_type.show, changes)
            end
          end
        end
      when Type::Symbol
        resolve_symbol_proc(changes, genv, ty.sym, blk_a_args, rbs_blk, param_map0)
      end
    end
  end

  force = true
  return true

ensure
  if force
    ret_vtx = method_type.return_type.covariant_vertex(genv, changes, param_map0)
    changes.add_edge(genv, ret_vtx, ret)
  end
end

#resolve_overloads(changes, genv, node, param_map, a_args, ret, &blk) ⇒ Object



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/typeprof/core/graph/box.rb', line 434

def resolve_overloads(changes, genv, node, param_map, a_args, ret, &blk)
  if @method_types.size == 1
    method_type = @method_types.first
    resolve_overload(changes, genv, method_type, node, param_map, a_args, ret, true, &blk)
    return
  end

  # If any positional argument has no type information, we cannot
  # determine which overload to select. Return silently (untyped)
  # rather than attempting to match. This prevents oscillation in
  # cyclic cases and avoids false "failed to resolve overloads"
  # diagnostics for untyped arguments.
  #
  # We check at two levels:
  # 1. Top-level empty vertices are always uninformative.
  # 2. Empty type parameter vertices (e.g., Array[T] where T is
  #    empty) are only uninformative when overloads differ solely
  #    in their type parameters (e.g., Array[Integer] vs
  #    Array[String]). When overloads differ at the top level
  #    (e.g., Integer vs Float), the type parameter contents are
  #    irrelevant for overload selection and should not trigger
  #    bail-out.
  has_uninformative_args = if @method_types.overloads_differ_in_args?
    # Check whether overloads also differ at the top level (e.g.,
    # Integer vs Float) or only in their type parameters (e.g.,
    # Array[Integer] vs Array[String]).
    if @method_types.overloads_differ_at_top_level?
      # Overloads are distinguished by top-level types.
      # Only top-level empty vertices matter; empty type parameters
      # are irrelevant for overload selection.
      # However, splatted arguments have their elements extracted
      # during matching, so also check splat element vertices.
      a_args.positionals.any? {|vtx| vtx.types.empty? } ||
        splat_elements_uninformative?(genv, a_args) ||
        (a_args.keywords && a_args.keywords.types.empty?)
    else
      # Overloads differ only in type parameters (e.g.,
      # Array[Integer] vs Array[String]). Empty type parameter
      # vertices can cause oscillation, so check recursively.
      a_args.positionals.any? {|vtx| vertex_uninformative?(genv, vtx) } ||
        (a_args.keywords && vertex_uninformative?(genv, a_args.keywords))
    end
  else
    a_args.positionals.any? {|vtx| vtx.types.empty? } ||
      (a_args.keywords && a_args.keywords.types.empty?)
  end
  if has_uninformative_args
    a_args.positionals.each do |vtx|
      changes.add_edge(genv, vtx, changes.target)
    end
    # Note: keywords already have a permanent edge to the box
    # (established in MethodCallBox#initialize), so no extra edge needed.
    return
  end

  # A splatted call can match only a rest-positional overload; otherwise prefer
  # the fixed-arity ones, as a rest-positional one is usually a catch-all
  overload_groups =
    a_args.splat_flags.any? ? [@method_types] : @method_types.partition_by_rest_positionals

  match_any_overload = false
  overload_groups.each do |method_types|
    method_types.each do |method_type|
      if resolve_overload(changes, genv, method_type, node, param_map, a_args, ret, false, &blk)
        match_any_overload = true
      end
    end
    break if match_any_overload
  end
  unless match_any_overload
    meth = node.mid_code_range ? :mid_code_range : :code_range
    changes.add_diagnostic(meth, "failed to resolve overloads")
  end
end

#resolve_symbol_proc(changes, genv, sym, blk_a_args, rbs_blk, param_map) ⇒ Object



427
428
429
430
431
432
# File 'lib/typeprof/core/graph/box.rb', line 427

def resolve_symbol_proc(changes, genv, sym, blk_a_args, rbs_blk, param_map)
  box = add_symbol_proc_call_box(changes, genv, sym, blk_a_args)
  return unless box

  rbs_blk.return_type.typecheck(genv, changes, box.ret, param_map)
end

#rest_keyword_args_typecheck?(genv, changes, keywords_vtx, method_type, param_map) ⇒ Boolean

Typecheck rest keyword argument values (those not consumed by named keywords) against the method type's rest_keywords type.

Returns:

  • (Boolean)


557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'lib/typeprof/core/graph/box.rb', line 557

def rest_keyword_args_typecheck?(genv, changes, keywords_vtx, method_type, param_map)
  named_keys = method_type.req_keyword_keys + method_type.opt_keyword_keys
  rest_ty = method_type.rest_keywords
  keywords_vtx.each_type do |kw_ty|
    case kw_ty
    when Type::Record
      kw_ty.fields.each do |key, val_vtx|
        next if named_keys.include?(key)
        return false unless rest_ty.typecheck(genv, changes, val_vtx, param_map)
      end
    when Type::Hash
      val_vtx = kw_ty.base_type(genv).args[1]
      return false if val_vtx && !rest_ty.typecheck(genv, changes, val_vtx, param_map)
    when Type::Instance
      if kw_ty.mod == genv.mod_hash && kw_ty.args[1]
        return false unless rest_ty.typecheck(genv, changes, kw_ty.args[1], param_map)
      end
    end
  end
  true
end

#showObject



579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'lib/typeprof/core/graph/box.rb', line 579

def show
  @method_types.map do |method_type|
    args = []
    method_type.req_positionals.each do |arg|
      args << arg.show
    end
    method_type.opt_positionals.each do |arg|
      args << "?#{arg.show}"
    end
    if method_type.rest_positionals
      args << "*#{method_type.rest_positionals.show}"
    end
    method_type.post_positionals.each do |arg|
      args << arg.show
    end

    method_type.req_keyword_keys.zip(method_type.req_keyword_values) do |key, arg|
      args << "#{ key }: #{arg.show}"
    end
    method_type.opt_keyword_keys.zip(method_type.opt_keyword_values) do |key, arg|
      args << "?#{ key }: #{arg.show}"
    end
    if method_type.rest_keywords
      args << "**#{method_type.rest_keywords.show}"
    end

    s = args.empty? ? "-> " : "(#{ args.join(", ") }) -> "
    s += method_type.return_type.show
  end.join(" | ")
end

#splat_elements_uninformative?(genv, a_args) ⇒ Boolean

Check if any splatted argument has an Array element vertex that is empty. Splat expansion extracts elements during overload matching, so empty element types can cause oscillation even when the top-level Array type is present.

Returns:

  • (Boolean)


513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/typeprof/core/graph/box.rb', line 513

def splat_elements_uninformative?(genv, a_args)
  a_args.positionals.each_with_index do |vtx, i|
    next unless a_args.splat_flags[i]
    vtx.each_type do |ty|
      base = ty.base_type(genv)
      if base.is_a?(Type::Instance) && base.mod == genv.mod_ary && base.args[0]
        return true if base.args[0].types.empty?
      end
    end
  end
  false
end

#vertex_uninformative?(genv, vtx, depth = 0) ⇒ Boolean

Returns:

  • (Boolean)


526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/typeprof/core/graph/box.rb', line 526

def vertex_uninformative?(genv, vtx, depth = 0)
  return true if vtx.types.empty?
  return false if depth > 3
  vtx.each_type do |ty|
    base = ty.base_type(genv)
    next unless base.is_a?(Type::Instance) && !base.args.empty?
    base.args.each do |arg_vtx|
      return true if arg_vtx && vertex_uninformative?(genv, arg_vtx, depth + 1)
    end
  end
  false
end