ありがとうございます。やはり Devel::Cover は実行時の情報をキャプチャーして活用している

Tool Interactions

Tool: Write /home/hkoba/db/monthly/202512/tmp/Devel--Cover/examples/logic_tree_builder.pl

#!/usr/bin/env perl
use strict;
use warnings;
use v5.12;

#
# 論理式の木構造を静的に構築するサンプル
# Devel::Cover の手法を参考に、B モジュールで OP ツリーを解析
#

package LogicNode;

# 論理式の木構造を表現するノード

sub new {
    my ($class, %args) = @_;
    return bless \%args, $class;
}

sub type     { $_[0]->{type} }
sub op       { $_[0]->{op} }
sub left     { $_[0]->{left} }
sub right    { $_[0]->{right} }
sub text     { $_[0]->{text} }
sub children { @{$_[0]->{children} // []} }

sub is_leaf {
    my $self = shift;
    return $self->{type} eq 'leaf';
}

sub to_string {
    my ($self, $indent) = @_;
    $indent //= 0;
    my $pad = "  " x $indent;

    if ($self->is_leaf) {
        return "${pad}LEAF: $self->{text}";
    }

    my @lines = ("${pad}$self->{type} [$self->{op}]");

    if ($self->{left}) {
        push @lines, "${pad}  LEFT:";
        if (ref $self->{left}) {
            push @lines, $self->{left}->to_string($indent + 2);
        } else {
            push @lines, "${pad}    $self->{left}";
        }
    }

    if ($self->{right}) {
        push @lines, "${pad}  RIGHT:";
        if (ref $self->{right}) {
            push @lines, $self->{right}->to_string($indent + 2);
        } else {
            push @lines, "${pad}    $self->{right}";
        }
    }

    return join("\n", @lines);
}

#==============================================================================
package LogicTreeBuilder;

use B qw(svref_2object OPf_KIDS);
use B::Deparse;

my %LOGIC_OPS = (
    and       => { symbol => '&&',  short_circuit => 'left_false' },
    andassign => { symbol => '&&=', short_circuit => 'left_false' },
    or        => { symbol => '||',  short_circuit => 'left_true' },
    orassign  => { symbol => '||=', short_circuit => 'left_true' },
    dor       => { symbol => '//',  short_circuit => 'left_defined' },
    dorassign => { symbol => '//=', short_circuit => 'left_defined' },
    xor       => { symbol => 'xor', short_circuit => 'none' },
);

sub new {
    my $class = shift;
    my $self = bless {
        deparse => B::Deparse->new('-x7'),  # -x7: 最小限の変形
    }, $class;
    return $self;
}

#------------------------------------------------------------------------------
# コードリファレンスから論理式の木を構築
#------------------------------------------------------------------------------
sub build_from_coderef {
    my ($self, $coderef) = @_;

    my $cv = svref_2object($coderef);
    return unless $cv->isa('B::CV');

    $self->{deparse}{curcv} = $cv;
    $self->{trees} = [];

    my $root = $cv->ROOT;
    return [] unless $root && !$root->isa('B::NULL');

    $self->_scan_for_logic($root);

    return $self->{trees};
}

#------------------------------------------------------------------------------
# 論理演算を探して木を構築
#------------------------------------------------------------------------------
sub _scan_for_logic {
    my ($self, $op) = @_;

    return unless $op && !$op->isa('B::NULL');

    my $name = $op->name;

    if (exists $LOGIC_OPS{$name}) {
        my $tree = $self->_build_logic_tree($op);
        push @{$self->{trees}}, $tree if $tree;
    } elsif ($name eq 'cond_expr') {
        my $tree = $self->_build_cond_tree($op);
        push @{$self->{trees}}, $tree if $tree;
    }

    # 子ノードを再帰的に走査
    if ($op->flags & OPf_KIDS) {
        for (my $kid = $op->first; $kid && !$kid->isa('B::NULL'); $kid = $kid->sibling) {
            $self->_scan_for_logic($kid);
        }
    }
}

#------------------------------------------------------------------------------
# 論理演算の木を構築 (再帰的に部分式も解析)
#------------------------------------------------------------------------------
sub _build_logic_tree {
    my ($self, $op) = @_;

    my $name = $op->name;
    my $info = $LOGIC_OPS{$name} or return;

    my $left_op = $op->first;
    return unless $left_op && !$left_op->isa('B::NULL');

    my $right_op = $left_op->sibling;
    return unless $right_op && !$right_op->isa('B::NULL');

    # 左辺と右辺を再帰的に解析
    my $left  = $self->_analyze_operand($left_op);
    my $right = $self->_analyze_operand($right_op);

    return LogicNode->new(
        type          => $name,
        op            => $info->{symbol},
        short_circuit => $info->{short_circuit},
        left          => $left,
        right         => $right,
        right_const   => $self->_is_constant($right_op),
    );
}

#------------------------------------------------------------------------------
# 三項演算子の木を構築
#------------------------------------------------------------------------------
sub _build_cond_tree {
    my ($self, $op) = @_;

    my $cond_op = $op->first;
    return unless $cond_op && !$cond_op->isa('B::NULL');

    my $true_op = $cond_op->sibling;
    my $false_op = $true_op ? $true_op->sibling : undef;

    return LogicNode->new(
        type      => 'cond_expr',
        op        => '?:',
        condition => $self->_analyze_operand($cond_op),
        left      => $self->_analyze_operand($true_op),
        right     => $self->_analyze_operand($false_op),
    );
}

#------------------------------------------------------------------------------
# オペランドを解析 (論理演算なら再帰、そうでなければ葉ノード)
#------------------------------------------------------------------------------
sub _analyze_operand {
    my ($self, $op) = @_;

    return '' unless $op && !$op->isa('B::NULL');

    my $name = $op->name;

    # 論理演算子なら再帰的に木を構築
    if (exists $LOGIC_OPS{$name}) {
        return $self->_build_logic_tree($op);
    }

    # null OP の場合は中身を見る
    if ($name eq 'null') {
        my $first = $op->first;
        if ($first && !$first->isa('B::NULL')) {
            my $inner_name = $first->name;
            if (exists $LOGIC_OPS{$inner_name}) {
                return $self->_build_logic_tree($first);
            }
        }
    }

    # 葉ノード: deparse してテキストを取得
    return LogicNode->new(
        type => 'leaf',
        text => $self->_deparse_simple($op),
    );
}

#------------------------------------------------------------------------------
# シンプルな deparse (論理演算子は展開しない)
#------------------------------------------------------------------------------
sub _deparse_simple {
    my ($self, $op) = @_;

    return '' unless $op && !$op->isa('B::NULL');

    local $@;
    my $text = eval { $self->{deparse}->deparse($op, 0) };
    return $text // '' if $@;

    # 後処理
    $text =~ s/^\s+//;
    $text =~ s/\s+$//;

    return $text;
}

#------------------------------------------------------------------------------
# 定数判定
#------------------------------------------------------------------------------
sub _is_constant {
    my ($self, $op) = @_;

    return 0 unless $op && !$op->isa('B::NULL');

    my $name = $op->name;
    return 1 if $name =~ /^(?:const|undef|s?refgen|null)$/;
    return 1 if $name =~ /^(?:die|return|last|next|redo|goto)$/;
    return 0;
}

#==============================================================================
package TruthTableGenerator;

#------------------------------------------------------------------------------
# LogicNode から真理値表を生成
#------------------------------------------------------------------------------
sub generate {
    my ($class, $node) = @_;

    return [] unless $node;

    if ($node->is_leaf) {
        return $class->_leaf_table($node);
    }

    my $type = $node->type;

    if ($type eq 'cond_expr') {
        return $class->_cond_table($node);
    }

    # 左右のオペランドの変数を収集
    my @vars = $class->_collect_variables($node);

    # 左右が複合式かどうかで分岐
    my $left_complex  = ref($node->left)  && !$node->left->is_leaf;
    my $right_complex = ref($node->right) && !$node->right->is_leaf;

    if ($left_complex || $right_complex) {
        return $class->_complex_table($node, \@vars);
    }

    return $class->_simple_table($node);
}

#------------------------------------------------------------------------------
# 変数を収集
#------------------------------------------------------------------------------
sub _collect_variables {
    my ($class, $node) = @_;

    return () unless $node;

    if ($node->is_leaf) {
        my $text = $node->text;
        # $var 形式の変数を抽出
        my @vars = $text =~ /(\$\w+)/g;
        return @vars;
    }

    my @vars;
    push @vars, $class->_collect_variables($node->left)  if ref $node->left;
    push @vars, $class->_collect_variables($node->right) if ref $node->right;

    # 重複を除去しつつ順序を保持
    my %seen;
    return grep { !$seen{$_}++ } @vars;
}

#------------------------------------------------------------------------------
# 単純な真理値表 (and_3, or_3 など)
#------------------------------------------------------------------------------
sub _simple_table {
    my ($class, $node) = @_;

    my $type = $node->type;
    my $op   = $node->op;

    my $left_text  = ref($node->left)  ? $node->left->text  : $node->left;
    my $right_text = ref($node->right) ? $node->right->text : $node->right;

    my $right_const = $node->{right_const};

    my @table;

    if ($type =~ /^(?:and|andassign)$/) {
        if ($right_const) {
            # and_2
            @table = (
                { A => 0, result => 0, path => "!$left_text (short-circuit)" },
                { A => 1, result => 1, path => "$left_text" },
            );
        } else {
            # and_3
            @table = (
                { A => 0, B => 'X', result => 0, path => "!$left_text (short-circuit)" },
                { A => 1, B => 0,   result => 0, path => "$left_text $op !$right_text" },
                { A => 1, B => 1,   result => 1, path => "$left_text $op $right_text" },
            );
        }
    } elsif ($type =~ /^(?:or|orassign|dor|dorassign)$/) {
        if ($right_const) {
            # or_2
            @table = (
                { A => 1, result => 1, path => "$left_text (short-circuit)" },
                { A => 0, result => 0, path => "!$left_text" },
            );
        } else {
            # or_3
            @table = (
                { A => 1, B => 'X', result => 1, path => "$left_text (short-circuit)" },
                { A => 0, B => 1,   result => 1, path => "!$left_text $op $right_text" },
                { A => 0, B => 0,   result => 0, path => "!$left_text $op !$right_text" },
            );
        }
    } elsif ($type eq 'xor') {
        # xor_4 (短絡評価なし)
        @table = (
            { A => 0, B => 0, result => 0, path => "!$left_text xor !$right_text" },
            { A => 0, B => 1, result => 1, path => "!$left_text xor $right_text" },
            { A => 1, B => 0, result => 1, path => "$left_text xor !$right_text" },
            { A => 1, B => 1, result => 0, path => "$left_text xor $right_text" },
        );
    }

    return {
        expression => "$left_text $op $right_text",
        type       => $type,
        headers    => $right_const ? ['A'] : ['A', 'B'],
        rows       => \@table,
    };
}

#------------------------------------------------------------------------------
# 複合式の真理値表
#------------------------------------------------------------------------------
sub _complex_table {
    my ($class, $node, $vars) = @_;

    # 全変数の組み合わせを生成
    my $n = @$vars;
    my @table;

    for my $i (0 .. (2**$n - 1)) {
        my %vals;
        for my $j (0 .. $n-1) {
            $vals{$vars->[$j]} = ($i >> ($n - 1 - $j)) & 1;
        }

        my $result = $class->_evaluate($node, \%vals);
        my @inputs = map { $vals{$_} } @$vars;

        push @table, {
            inputs => \@inputs,
            result => $result,
            vals   => \%vals,
        };
    }

    return {
        expression => $class->_node_to_expr($node),
        variables  => $vars,
        headers    => [map { s/^\$//r } @$vars],
        rows       => \@table,
    };
}

#------------------------------------------------------------------------------
# 論理式を評価
#------------------------------------------------------------------------------
sub _evaluate {
    my ($class, $node, $vals) = @_;

    return 0 unless $node;

    if ($node->is_leaf) {
        my $text = $node->text;
        # $var 形式を探して値を返す
        if ($text =~ /^\$(\w+)$/) {
            return $vals->{$text} // 0;
        }
        # 定数として評価
        return $text ? 1 : 0;
    }

    my $type = $node->type;
    my $left_val  = $class->_evaluate($node->left, $vals);
    my $right_val = $class->_evaluate($node->right, $vals);

    if ($type =~ /^(?:and|andassign)$/) {
        return $left_val && $right_val ? 1 : 0;
    } elsif ($type =~ /^(?:or|orassign|dor|dorassign)$/) {
        return $left_val || $right_val ? 1 : 0;
    } elsif ($type eq 'xor') {
        return ($left_val xor $right_val) ? 1 : 0;
    } elsif ($type eq 'cond_expr') {
        my $cond_val = $class->_evaluate($node->{condition}, $vals);
        return $cond_val ? $left_val : $right_val;
    }

    return 0;
}

#------------------------------------------------------------------------------
# ノードを式文字列に変換
#------------------------------------------------------------------------------
sub _node_to_expr {
    my ($class, $node) = @_;

    return '' unless $node;

    if ($node->is_leaf) {
        return $node->text;
    }

    my $left  = $class->_node_to_expr($node->left);
    my $right = $class->_node_to_expr($node->right);
    my $op    = $node->op;

    return "($left $op $right)";
}

#------------------------------------------------------------------------------
# 葉ノード用
#------------------------------------------------------------------------------
sub _leaf_table {
    my ($class, $node) = @_;
    return { expression => $node->text, type => 'leaf', rows => [] };
}

#------------------------------------------------------------------------------
# 三項演算子用
#------------------------------------------------------------------------------
sub _cond_table {
    my ($class, $node) = @_;

    my $cond_text = ref($node->{condition}) ? $class->_node_to_expr($node->{condition}) : $node->{condition};
    my $true_text = ref($node->left) ? $class->_node_to_expr($node->left) : $node->left;
    my $false_text = ref($node->right) ? $class->_node_to_expr($node->right) : $node->right;

    return {
        expression => "$cond_text ? $true_text : $false_text",
        type       => 'cond_expr',
        headers    => ['cond'],
        rows       => [
            { cond => 1, result => $true_text,  path => "condition is true" },
            { cond => 0, result => $false_text, path => "condition is false" },
        ],
    };
}

#------------------------------------------------------------------------------
# テキスト形式で出力
#------------------------------------------------------------------------------
sub format_text {
    my ($class, $table) = @_;

    return '' unless $table && $table->{rows} && @{$table->{rows}};

    my @lines;
    push @lines, "Expression: $table->{expression}";
    push @lines, "Type: $table->{type}" if $table->{type};
    push @lines, "-" x 70;

    my @headers = @{$table->{headers} // []};

    if ($table->{variables}) {
        # 複合式
        push @lines, sprintf("| %s | result |", join(" | ", @headers));
        push @lines, "-" x 70;

        for my $row (@{$table->{rows}}) {
            my $inputs = join(" | ", @{$row->{inputs}});
            push @lines, sprintf("| %s |   %d    |", $inputs, $row->{result});
        }
    } else {
        # 単純式
        push @lines, sprintf("| %s | result | path", join(" | ", @headers));
        push @lines, "-" x 70;

        for my $row (@{$table->{rows}}) {
            my @vals = map { $row->{$_} // 'X' } @headers;
            my $inputs = join(" | ", @vals);
            push @lines, sprintf("| %s |   %s    | %s",
                $inputs, $row->{result}, $row->{path} // '');
        }
    }

    push @lines, "-" x 70;
    return join("\n", @lines);
}

#==============================================================================
package main;

# テスト用サブルーチン
sub test1 { my ($a, $b) = @_; $a && $b }
sub test2 { my ($a, $b) = @_; $a || $b }
sub test3 { my ($a, $b, $c) = @_; ($a && $b) || $c }
sub test4 { my ($a, $b, $c) = @_; $a && ($b || $c) }
sub test5 { my ($a, $b, $c, $d) = @_; ($a || $b) && ($c || $d) }
sub test6 { my ($x) = @_; $x ? 1 : 0 }

my $builder = LogicTreeBuilder->new;

print "=" x 70, "\n";
print "Logic Tree Builder - Static Analysis Demo\n";
print "=" x 70, "\n\n";

my @tests = (
    [\&test1, '$a && $b'],
    [\&test2, '$a || $b'],
    [\&test3, '($a && $b) || $c'],
    [\&test4, '$a && ($b || $c)'],
    [\&test5, '($a || $b) && ($c || $d)'],
    [\&test6, '$x ? 1 : 0'],
);

for my $test (@tests) {
    my ($coderef, $desc) = @$test;

    print "### $desc\n\n";

    my $trees = $builder->build_from_coderef($coderef);

    for my $tree (@$trees) {
        print "Logic Tree:\n";
        print $tree->to_string(1), "\n\n";

        my $table = TruthTableGenerator->generate($tree);
        print TruthTableGenerator->format_text($table), "\n\n";
    }

    print "-" x 70, "\n\n";
}

Results

Result:

File created successfully at: /home/hkoba/db/monthly/202512/tmp/Devel--Cover/examples/logic_tree_builder.pl

より完成度の高いバージョンを作成しました。実行して結果を確認してみましょう。

Tool: Bash

{
  "command": "perl examples/logic_tree_builder.pl 2>&1",
  "description": "Run logic tree builder"
}

Results

Result:

======================================================================
Logic Tree Builder - Static Analysis Demo
======================================================================

### $a && $b

Logic Tree:
  and [&&]
    LEFT:
      LEAF: $a
    RIGHT:
      LEAF: $b

Expression: $a && $b
Type: and
----------------------------------------------------------------------
| A | B | result | path
----------------------------------------------------------------------
| 0 | X |   0    | !$a (short-circuit)
| 1 | 0 |   0    | $a && !$b
| 1 | 1 |   1    | $a && $b
----------------------------------------------------------------------

----------------------------------------------------------------------

### $a || $b

Logic Tree:
  or [||]
    LEFT:
      LEAF: $a
    RIGHT:
      LEAF: $b

Expression: $a || $b
Type: or
----------------------------------------------------------------------
| A | B | result | path
----------------------------------------------------------------------
| 1 | X |   1    | $a (short-circuit)
| 0 | 1 |   1    | !$a || $b
| 0 | 0 |   0    | !$a || !$b
----------------------------------------------------------------------

----------------------------------------------------------------------

### ($a && $b) || $c

Logic Tree:
  or [||]
    LEFT:
      and [&&]
        LEFT:
          LEAF: $a
        RIGHT:
          LEAF: $b
    RIGHT:
      LEAF: $c

Expression: (($a && $b) || $c)
----------------------------------------------------------------------
| a | b | c | result |
----------------------------------------------------------------------
| 0 | 0 | 0 |   0    |
| 0 | 0 | 1 |   1    |
| 0 | 1 | 0 |   0    |
| 0 | 1 | 1 |   1    |
| 1 | 0 | 0 |   0    |
| 1 | 0 | 1 |   1    |
| 1 | 1 | 0 |   1    |
| 1 | 1 | 1 |   1    |
----------------------------------------------------------------------

Logic Tree:
  and [&&]
    LEFT:
      LEAF: $a
    RIGHT:
      LEAF: $b

Expression: $a && $b
Type: and
-------------------------------------------
... (truncated)

素晴らしい!うまく動作しています。論理式の木構造が正しく解析され、真理値表も生成されています。

ユーザーにサンプルコードの概要と、Devel::Cover との違いを説明しましょう。


uuid: assistant: 8f0c2413-893b-437f-85cc-244f015da68c, assistant: 517abd0d-5fd9-40df-8d2e-fc30b432c361, assistant: f4b1e88e-30af-4963-8c7c-ea1a9adb7729, assistant: df1427ae-14f2-423a-a66d-90baea54990f