TypeProf Documentation

repository·master·Indexed 21 days ago

https://github.com/ruby/typeprof

An experimental type-level Ruby interpreter for testing and understanding Ruby code through abstract interpretation. TypeProf tracks abstract values instead of concrete objects to observe method types and generate RBS definitions. It features a CLI for batch analysis, a Language Server Protocol (LSP) implementation for editor integration (including VSCode), and a scenario-based testing framework for verifying type inference correctness. Requires Ruby 3.3 or later.

Tokens
7.9K
Snippets
25
Records
43
Agent score
71%

What's inside TypeProf

  1. What is a Scenario File?

    master
    A scenario file is a specialized Ruby file used to describe TypeProf's behavior. It contains input Ruby code and the expected type inference results. These files are used to reproduce bugs, propose new features, and verify TypeProf's correctness. You can find existing examples in the scenario/ directory of the repository.
  2. What are abstract values in TypeProf?

    master

    To avoid confusion with standard Ruby values, TypeProf uses the term abstract values to describe the entities it handles during type-level execution.

    TypeProf supports the following abstract values:

    • Instance of a class: Represents an object of a specific class (e.g., Foo.new returns the abstract value Foo). Note that literals like 42 are treated as instances of Integer.
    • Class object: Represents the class itself (e.g., the constant Integer). TypeProf does not abstract these to the Class type to ensure constant references and class methods are handled correctly.
    • Symbol: Represents symbol literals (e.g., :foo). Unlike other objects, symbols are often handled by their concrete value to support use cases like keyword arguments or JSON keys.
    • untyped: An abstract value used when TypeProf cannot trace a value due to analysis limits. Operations on untyped result in untyped.
    • Union of abstract values: Represents multiple possibilities for a value (e.g., Integer | String).
    • Instance of a container class: Represents objects that contain other abstract values. Currently supported: Array, Hash, and Enumerator.
    • Proc object: Represents closures from lambda expressions (-> {... }) or block parameters (&blk). These are treated as concrete pieces of code and represented in RBS using anonymous proc types.
  3. Understand TypeProf abstract values

    master

    To avoid losing precision, TypeProf uses "abstract values" instead of actual Ruby objects. Most values are abstracted to their class.

    Supported abstract values include:

    • Class instances: The most common value (e.g., 42 is an Integer instance). In RBS output, these are represented by the class name (e.g., Foo).
    • Class objects: Represents the classes themselves (e.g., the constant Integer). These are not abstracted to Class to preserve constant references and class methods.
    • Symbols: Literal symbols (e.g., :foo) are kept concrete. However, symbols generated via String#to_sym or interpolated symbols (e.g., :"foo_#{x}") are treated as the Symbol class.
    • untyped: Used when TypeProf cannot track a value due to analysis limits. Operations on untyped result in untyped.
    • Unions: Represents multiple possibilities (e.g., Integer | String).
    • Container class instances: Currently supports Array, Hash, and Enumerator.
    • Proc objects: Closures (lambdas or block arguments) are treated as concrete values tied to code fragments to track arguments and return values.
  4. How TypeProf performs analysis

    master

    TypeProf is an interpreter that executes Ruby programs at a type level. Instead of tracking actual object values, it tracks "abstract values" (primarily the classes the objects belong to).

    Key behaviors:

    • Method Analysis: It observes what types are passed to methods and what types are returned, then generates RBS definitions.
    • Revealed Types: It outputs observations of values (e.g., arguments to p) under a # Revealed types section.
    • Instance Variables: Unlike standard Ruby where instance variables are unique to each object, TypeProf aggregates them at the class level. If different instances assign different types to the same instance variable, TypeProf represents this as a Union type (e.g., Integer | String).
    def foo(n)
      p n      #=> Integer
      n.to_s
    end
    
    p foo(42)  #=> String

    Analysis Output Example:

    # Revealed types
    #  test.rb:2 #=> Integer
    #  test.rb:6 #=> String
    
    # Classes
    class Object
      def foo : (Integer) -> String
    end
  5. How TypeProf works via abstract interpretation

    master

    TypeProf is a Ruby interpreter that performs abstract interpretation. Instead of executing code with concrete values (like the number 42), it executes the program at the type level using abstract values.

    The Analysis Process

    1. Observation: As TypeProf executes the code, it observes what types are passed to methods, what types are returned from methods, and what types are assigned to instance variables.
    2. Aggregation: While Ruby stores instance variables per object, TypeProf aggregates these values at the class level. If different instances of a class assign different types to the same instance variable, TypeProf represents that variable as a union type.
    3. Output: TypeProf produces two main types of output:
      • Revealed types: A log showing the types observed at specific lines of code (e.g., test.rb:2 #=> Integer).
      • RBS format: A structured representation of the discovered class hierarchies and method signatures.

    Example: Union Types in Instance Variables

    If a class Foo has an accessor a, and one instance sets a to an Integer while another sets it to a String, TypeProf reveals the type as a union:

    class Foo
      def initialize
        @a = 42
      end
      attr_accessor :a
    end
    
    Foo.new.a = "str"
    p Foo.new.a #=> Integer | String

    Output:

    # Revealed types
    #  test.rb:11 #=> Integer | String
    
    # Classes
    class Foo
      attr_accessor a : Integer | String
      def initialize : -> Integer
    end
  6. Writing a Scenario File

    master

    Scenario files use specific section headers to define input, expectations, and diagnostic requirements.

    Basic Pattern

    A minimal file requires two sections:

    • ## update: Contains the input Ruby code.
    • ## assert: Contains the expected type signatures written in RBS syntax.

    Multiple Updates

    You can chain multiple ## update and ## assert pairs within a single file to test how TypeProf handles sequential code changes or different implementation variations.

    Diagnostics

    To verify that TypeProf correctly identifies type errors, use the ## diagnostics section. This section should contain the expected error messages and their locations in the format (line,column)-(line,column): error_message.

    ## update
    def foo(n)
      n.to_s
    end
    
    foo(42)
    
    ## assert
    class Object
      def foo: (Integer) -> String
    end
    
    ## diagnostics
    (5,0)-(5,3): wrong number of arguments (2 for 1)
  7. Report a Bug in TypeProf

    master

    When reporting a bug, follow these steps to ensure the maintainers can reproduce and fix the issue:

    1. Create a scenario file that reproduces the reported issue.
    2. Verify the scenario by running it with ruby tool/scenario_runner.rb your_scenario.rb to confirm it fails as expected.
    3. Open a GitHub issue and include the following information:
      • Your TypeProf version (obtain via typeprof --version).
      • The full content of your scenario file.
      • A clear description of what you expected to happen versus what actually happened.
  8. Quick start with VSCode integration

    master

    To use TypeProf within VSCode for type-level Ruby interpretation:

    1. Install the Ruby TypeProf extension via CLI: code --install-extension mame.ruby-typeprof
    2. Initialize your project by running typeprof --init in your project root. This creates a typeprof.conf.jsonc configuration file.
    3. Reopen your project in VSCode to enable the extension features.
    $ typeprof --init
  9. Initialize TypeProf configuration

    master

    Run typeprof --init in your project root to generate a typeprof.conf.jsonc file. This file is used to configure TypeProf's behavior. For detailed configuration options, refer to the typeprof.conf.jsonc file template.

    $ typeprof --init