Make ツールの再帰展開変数の真似

設定ファイルで変数を扱う場合を考えて、make(1) の再帰展開変数と同じやりかたで変数を展開するおもちゃを Perl で書いてみます。

再帰展開変数は、定義時に展開をおこなわず、参照時に展開します。展開後にさらに変数参照が含まれるときは、再帰的に展開していきます。それだけでなく、変数名にも変数参照を含めることができます。無限ループを避けるため、展開にあたって、循環参照のチェックが必須で、循環参照を検出したらエラーとします。変数定義を参照の順番を気にせずに記述できるので、トップダウンで書き下せて記述の見通しが良くなるメリットがあります。

# 再帰展開変数を使った Makefile
# make すると
#
#     a B X1 C z
#
# と表示する。

a=$(b) $(c)
b=B
c=$(x_$(n)) C
n=1
x_1=X1

default:
        echo a $(a) z

コードを簡単にするため、変数参照は必ず括弧でくくる約束にします。 Perl で書くおもちゃでは、展開変数はハッシュで定義して、expand 関数に渡します。

use 5.016;
use strict;
use warnings;
use Carp;

my $rule = {
    'a' => parse('$(b) $(c)'),
    'b' => parse('B'),
    'c' => parse('$(x_$(n)) C'),
    'n' => parse('1'),
    'x_1' => parse('X1'),
};

括弧の入れ子を扱うため、文字列で記述された参照式を parse で入れ子リストへ変換した上で扱うことにします。リストの要素はスカラーか配列リファレンスであり、変数参照は配列リファレンスで表します。

# 例: parse('$(x_$(n)) C') => [['$(', 'x_', ['$(', 'n', ')'], ')'], ' C']
sub parse {
    my($s) = @_;
    my $e = [[]];
    while ($s =~ m/\G(\$\(|\)|[^\$\)]+)/gcmsx) {
        my($t) = $1;
        if ($t eq q[$(]) {
            my $x = [$t];
            push @{$e->[-1]}, $x;
            push @{$e}, $x;
        }
        elsif ($t eq q[)]) {
            if (! @{$e->[-1]} || $e->[-1][0] ne q[$(]) {
                croak "syntax error: $s: missing '\$('."
            }
            push @{$e->[-1]}, $t;
            pop @{$e};
        }
        else {
            push @{$e->[-1]}, $t;
        }
    }
    @{$e} == 1 or croak "syntax error: $s: not close '\$( ... )'.";
    return $e->[0];
}

最初に再帰呼び出しの継続渡しスタイル (CPS) で変数展開を記述してみます。変数参照は、最初に変数名を展開した後に、変数の値の展開をおこないます。このとき、循環参照のチェックのため、展開済みの変数名を mark に記録しながら、再帰的に展開をおこなっていきます。注意しなければならないのは、値の展開時に mark を非破壊で拡張しなければならないことです。循環参照でないなら、同じ変数を参照式の中で何度参照しても構わないためです。もしも、ここで mark を破壊操作してしまうと、参照式の中で変数を一度しか展開できなくなってしまいます。

# CPS 版
say expand_cps(parse('a $(a) z'), $rule, {}, sub{ $_[0] }); #=> 'a B X1 C z'

sub expand_cps {
    my($exp, $rule, $mark, $kont) = @_;
    my $t = q();
    for my $e (@{$exp}) {
        if (! ref $e) { # スカラー文字列
            $t .= $e;
            next;
        }
        $t .= expand_cps([@{$e}[1 .. $#{$e} - 1]], $rule, $mark, sub{ # 変数名の展開
            my($s) = @_;
            exists $mark->{$s} and croak "cycle reference '$s'.";
            exists $rule->{$s} or croak "undeclared variable '$s'.";
            return expand_cps($rule->{$s}, $rule, {%{$mark}, $s=>1}, sub{ $_[0] }); # 値の展開
        });
    }
    return $kont->($t);
}

ところで、上の場合、継続の最後でオーム返しをしているだけです。この場合、継続の中身をくくりだすことができます。

# 非 CPS 版
say expand(parse('a $(a) z'), $rule, {}); #=> 'a B X1 C z'

sub expand {
    my($exp, $rule, $mark) = @_;
    my $t = q();
    for my $e (@{$exp}) {
        if (! ref $e) { # スカラー文字列
            $t .= $e;
            next;
        }
        my $s = expand([@{$e}[1 .. $#{$e} - 1]], $rule, $mark); # 変数名の展開
        exists $mark->{$s} and croak "cycle reference '$s'.";
        exists $rule->{$s} or croak "undeclared variable '$s'.";
        $t .= expand($rule->{$s}, $rule, {%{$mark}, $s=>1}); # 値の展開
    }
    return $t;
}