PostScript 基礎文法最速マスター

基礎文法最速マスターがはやっているみたいなので便乗します。

ほんとは Lua について書くつもり……というか書いていたんですけど、完成するより前に 良いもの。悪いもの。: Lua基礎文法最速マスター があがってました。

最近、基礎文法最速マスターというプログラミング言語の解説が流行ってるようなので、便乗してみた。個人的にはC++Pythonの方が慣れ親しんでいるのだが、自分でも勉強できるように普段使っていない言語を書いてみることにした。

良いもの。悪いもの。: Lua基礎文法最速マスター

うんうん。わたしも勉強のために普段使ってない言語にするか。たまたまさいきん Ghostscript づいていたので PostScript にしようと思いました。

なにぶん初学者なのでいろいろ間違いがあればご指摘ください。とゆーか、詳しい人がよりよい最速マスターや補遺を書いてくださるとうれしいです。

下記のサイトを倣って書いてみました。

PostScript 自体については後述する文献を参考にしました。

1. 基礎

対話環境

Ghostscript の場合、そのまま実行すると対話環境として働きます。

$ gs
GPL Ghostscript 8.70 (2009-07-31)
Copyright (C) 2009 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
GS> (Hello, World!) =
Hello, World!
GS> 1 2 add
GS<1> =
3
GS>

GS> という部分がプロンプトです。GS<1> のようになっている場合、現在のスタックにたまっている数を示します。

真っ白なウィンドウが表示されたかと思いますが、これは PostScript がページ記述言語であるためです。記述されたページコンテンツがここに表示されるのです。ですが、とりあえずこれがうざいのなら gs -dNODISPLAY のように起動すれば、このページ表示用ウィンドウの生成を抑制できます。

quit を評価するか、CTRL + D を押下すると終了します。

スタック指向言語とは

(だいじなことだけどあとでかく)

スタックの表示(print 文に相当)

オペレータ == を評価すると、スタックトップを pop してきて生で表示します。末尾に改行が自動付与されます。

GS> (Hello, World!) ==
(Hello, World!)

オペレータ = を評価した場合、表示内容として妥当なものを表示します。他の LL 言語でいう to_string() 的なものを適用したうえで表示、みたいなイメージです。

GS> (Hello, World!) =
Hello, World!

オペレータ print を評価した場合は、= と同様ですが、末尾に改行が自動付与されません。

GS> (Hello, World!) print
Hello, World!GS>

つまり、本気で表示するのなら print を、デバッグなどなら === を使うことになります。ですが、これらはスタックトップの値の表示しかできません。スタック全体を見てみたい場合は、pstackstack というオペレータを使います。

GS> 1 2 (Fizz) 4 (Buzz)

GS<5> pstack            % 「==」を使ってスタックを表示
(Buzz)
4
(Fizz)
2
1

GS<5> stack             % 「=」を使ってスタックを表示
Buzz
4
Fizz
2
1

GS<5>

実行結果をみてわかるとおり、pstackstack は pop 作業をともないません。

コメント

一行コメントです。% から行末までがコメントアウトされます。

% コメント
変数の宣言

PostScript には変数の宣言はありません。変数に代入する型も制限されません。

というか、そもそも変数という呼び方をしません。ある「名前」(のトークン)に「オブジェクト」を「束縛(バインド)」する、といいます。束縛するにはオペレータ def を使います((なお、def オペレータの引数は lisp と逆順なので、引数を変数に設定する際などにちょっとめんどくさくなります(後述)。))。

/a 1 def
a ==
% => 1

/a (foo) def
a ==
% => (foo)

上記の例では、まず a という名前に 1 という値を束縛しています。冒頭で /a のように「/」がプレフィックスされているのは、「名前自身」をあらわすためです。単純に a と書くと、a というトークン自身が評価されてしまうので、def するときには「名前自身」を即値としてスタックに積んでおく必要があるわけです。

スクリプトの実行

スクリプトファイルは、慣例的に

%!PS-Adobe-3.0

という行で開始することになっています。

たとえば、

%!PS-Adobe-3.0
/var 123 456 add def
var ==

のようなファイルを test.ps という名前で保存します。

これを実行するには、

$ gs test.ps
GPL Ghostscript 8.70 (2009-07-31)
Copyright (C) 2009 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
579
GS>

のようにします。

実行後に GS> というプロンプトがでていますね。最後に終了するスクリプトを書くなら、最後に quit というオペレータを評価すればいいです。

ですが、今回の例で quit を記述しなかったのには理由があります。スクリプトは PostScript インタプリタを起動した上で run オペレータを使って読み込み・実行することができます。

$ gs
GPL Ghostscript 8.70 (2009-07-31)
Copyright (C) 2009 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
GS> (test.ps) run
579
GS> var ==
579
GS>

つまり、ライブラリとして使えるんですね。

2. 数値

整数や浮動小数点数が使えます。

1       ==      % => 1     整数
16#beaf ==      % => 48815 16進数 (2進数でも、8進数でも、3進数でも可)

3.14    ==      % => 3.14  小数
.3      ==      % => 0.3   
314e-2  ==      % => 3.14  指数表記
各種演算
1 2 add ==      % => 3
3 4 sub ==      % => -1
5 6 mul ==      % => 30
5 2 div ==      % => 2.5
5 2 idiv ==     % => 2    (整除算)
5 2 mod ==      % => 1    (剰余)
2 3 exp ==      % => 8.0  (累乗; 入出力は浮動小数点)

4 sqrt ==       % => 2.0  (平方根)
90 sin ==       % => 1.0  (三角関数; 単位は度)
% など、いろいろおなじみなオペレータはデフォルトで用意されています
インクリメント/デクリメント/加減

ありません。工夫して書く必要があります(たぶん)。

/foo 1 def

/foo foo 1 add def
foo ==                  % => 2

ちと汎用的ではないかもしれませんね。後述する内容を使うとこんな感じに書けるかな。

/add-to { 1 index load add def } def

/foo 1 add-to
foo ==                  % => 3

3. 文字列

文字列は丸括弧で囲みます。一般的なエスケープシーケンスも利用できます。行末に \ を置くことで、長い文字列を複数行にわたって記述することもできます。

(abc) =                 % => abc
(def\nghi) =            % => def [改行] ghi
(ijk\(lmn) =            % => ijk(lmn

(abc\
def\
ghi) =                  % => abcdefghi

ただしこのような文字列リテラルは英数字しか表現できません。日本語のようにハイビットをもつ文字表記集合の場合、< ... > の間に文字コードを記載する方法で表現する必要があります。

<e697a5e69cace8aa9e> =                  % => 日本語

% ホワイトスペースで区切ることもできます
<e6 97 a5  e6 9c ac  e8 aa 9e> =        % => 日本語

文字列関連のオペレータです。

% 結合系
% ……要調査……

% 分割 (トークナイズ)
(foo bar baz) token pstack
% => true               % 成否
%    foo                % トーク
%    (bar baz)          % 残り文字列

% 長さ (バイト数)
(Supercalifragilisticexpialidocious) length ==  % => 34

% 切り出し/置換
(0123456789) 3 get ==                           % => 51
(0123456789) 3 4 getinterval ==                 % => (3456)
/s (0123456789) def s 3 88 put            s ==  % => (012X456789)
/s (0123456789) def s 3 (XYZ) putinterval s ==  % => (012XYZ6789)

% 検索
(0123456789) (345) search pstack                % 部分文字列検索
% => true               % 成否
%    (012)              % pre
%    (345)              % hit
%    (6789)             % post

(0123456789) (345) anchorsearch pstack          % 先頭にマッチするか
% => false              % 成否
%    (0123456789)       % 元文字列 (失敗したので元文字列だけ返る)

4. スタック操作

スタック指向だけあってスタック操作のオペレータは充実しています。

clear                   % スタックを空にする

1 2 3 4                 % トークンを書くと push されるので push というオペレータはない
pstack
% => 4
%    3
%    2
%    1

pop                     % pop
pstack
% => 3
%    2
%    1

exch                    % スタック上位2要素を交換する
pstack
% => 2
%    3
%    1

dup                     % スタックトップの複製
pstack
% => 2
%    2
%    3
%    1

2 index                 % スタックトップから2+1番めの要素をコピー
% => 3
%    2
%    2
%    3
%    1

2 copy                  % スタックトップから指定数分の要素をコピー
% => 3
%    2
%    3
%    2
%    2
%    3
%    1

count ==                % 現在のスタックの深さを push
% => 7

exch というオペレータはスタックトップ2要素の交換なのですが、現在スタックトップにある要素を束縛するときなどよく使います。

3 4 add
pstack
% => 7

/var                    % /var という名前に束縛したいけど
pstack
% => /var               % このまま def できない
%    7

exch
pstack
% => 7                  % def するときのスタックの並び順になった
%    /var

def

var ==
% => 7

roll というオペレーターはスタックをローテートします。

clear
1 2 3 4 5
4 1 roll                % 上位4要素を上に1つローテート
pstack
% => 4
%    3
%    2
%    5
%    1

4 -2 roll               % 上位4要素を下に2つローテート
pstack
% => 2
%    5
%    4
%    3
%    1

マーカー関連のオペレータです。

clear

1 2 mark 3 4 5          % マーカーを push
pstack
% => 5
%    4
%    3
%    -mark-
%    2
%    1

counttomark ==          % スタックトップからマーカーのある位置までの要素をカウント
% => 3

cleartomark             % スタックトップからマーカーまでを消去
% => 2
%    1

5. 配列

[] というオペレータ((実際は [ というオペレータは mark というオペレータと同義で、スタックにマーク(というもの)を push しているだけです。Unixtest コマンドみたいですね。))で配列を生成できます。

[ 1 (x) 3 ] ==            % => [1 (x) 3]

array というオペレータを用いて空の配列を生成することもできます。

[ ]             % 空の配列
0 array         % 初期サイズ0の配列を生成
3 array         % 初期サイズ3の配列を生成(配列自体は空; 必要に応じて拡張はされる)

配列まわりのオペレータです。

/a [ 0 1 2 ] def
a length ==             % => 3 (要素の個数)

/b a def                % エイリアス (束縛しているだけなのであたりまえ)
b 1 get ==              % => 1
b 1 (xyz) put
b ==                    % => [0 (xyz) 2]
a ==                    % => [0 (xyz) 2]  (エイリアスだから)

% 部分配列の取得
/a [ 0 1 2 3 4 5 6 7 8 9 ] def
a 3 4 getinterval ==    % => [3 4 5 6]

% 部分配列の置換
a 3 [ (X) (Y) (Z) ] putinterval
a ==                    % => [ 0 1 2 (X) (Y) (Z) 6 7 8 9 ]

% スタックへの変換
[ 0 1 2 ] aload pstack
% => [0 1 2]
%    2
%    1
%    0

% スタックからの変換
(X) (Y) (Z)     % スタックにつんでおく
3 array         % サイズ3の空配列の生成
astore ==       % => [(X) (Y) (Z)]

% 配列の push, pop
% ……要調査……

6. 辞書

<<>> というオペレータで辞書リテラルを生成することができます。

<< /foo 1 /var (xyz) >> ==
% => -dict-     % 辞書は == では dump されず dict オブジェクトであることが示されるのみ

dict というオペレータを用いて空の辞書を生成することもできます。

<< >>           % 空の辞書
0 dict          % 初期サイズ0の辞書を生成
3 dict          % 初期サイズ3の辞書を生成(辞書自体は空; 必要に応じて拡張はされる)

辞書を操作するオペレータたちです。

/d << /foo 1 /bar (xyz) >> def

% 要素の取得
d /foo get ==         % => 1
d /bar get ==         % => (xyz)
d /baz get ==         % => ERROR: /undefined in --execute-- (容赦ねー)

d /foo known ==       % => true
d /bar undef          % 削除
d /bar known ==       % => false

% 要素の代入
d /foo (hoge) put
d /foo get ==         % => (hoge)
d /baz (fuga) put
d /baz get ==         % => (fuga)

% キーの列挙などの組み込みオペレータはないので工夫する必要があります
[ d { pop } forall ] ==
% => [/baz /foo]
余談: 辞書のキー

実は辞書のキーは null 以外の任意のオブジェクトを指定できます。

<< [ (foo) (bar) ] 123 >> { pop == } forall     % キーを列挙
% => [(foo) (bar)]

んで、名前ベースで辞書を検索する際、文字列がキーの場合も同様に検索されるようです。

/foo 123 def
foo ==          % => 123

(foo) 345 def
foo ==          % => 345  上書きされた

慣例的に束縛をおこなう際は /foo 表記を使うのですが、Ruby でいうシンボルみたいなものだと考えればいいのかな。ちょっとよくわかりません。名前リテラルはたぶん immutable なのかな。

カレント辞書と辞書スタック

実はこれまで def で名前に束縛してきましたが、これは現在の操作対象の辞書(カレント辞書)にたいして束縛をおこなっていたのでした。begin というオペレータをもちいるとカレント辞書を変更することができます。いうなれば、JavaScriptwith 文のようなものです。

begin があるのに対応して end というオペレータもあります。これはさきほどまでのカレント辞書に戻す、というものです。つまり、辞書についても辞書用のスタックがあるのです。JavaScript でたとえるなら with 文をネストしたような感じと思えばいいでしょう。

辞書スタックをさかのぼって値を取得・設定するオペレータもあります。さきほどの getput に対応するものが、それぞれ loadstore になります。

/foo (foo) def          % 辞書[0] に設定
/hoge (hoge) def
foo ==                  % => (foo)
hoge ==                 % => (hoge)


<< /foo (bar) >> begin  % 辞書[1]
foo ==                  % => (bar)
hoge ==                 % => (hoge); さかのぼって検索・評価される
/foo (baz) def          % 辞書[1] に /foo の値を設定 (def だからカレント辞書に)
foo ==                  % => (baz)


0 dict begin            % 辞書[2]
/foo load ==            % => (baz); load もさかのぼって検索する

%currentdict /foo get ==
% => Error: /undefined in --execute--
%    カレント辞書に /foo キーの項目があるわけではない

/foo (ban) def          % 辞書[2] に登録
/hoge (fuga) store      % さかのぼって登録 (この場合、/hoge キーのある辞書[0])

end                     % 辞書[1] に戻る


foo ==                  % => (baz)   ; (ban) ではないことに注意
hoge ==                 % => (fuga)

end                     % 辞書[0] に戻る


foo ==                  % => (foo)
hoge ==                 % => (fuga)

def はあくまでカレント辞書において束縛するのに対し、store は辞書スタックをさかのぼって一番最初に名前が定義されていた辞書において束縛するところが違います*1

これらを応用すると後述するローカル変数のようなものを実現できます。

7. 手続き (関数)

{} で囲むと、その間のトークンが「実行可能配列」としてひとまとまりの実行可能オブジェクトになります。簡単にいえば無名関数のようなものです。

{ (1 + 2 is ) print 1 2 add = }
pstack
% => {(1 + 2 is ) print 1 2 add =}

% exec オペレータを使うとスタックトップの実行可能オブジェクトを実行します。
exec            % => 1 + 2 is 3

def で名前に束縛すれば、名前つき手続きとして使えます。

/hello { (Hello, World!) = } def

hello
% => Hello, World!

スタックから値を取得して引数とすることもできます。

/greeting { (Hello, ) print print ( san!\n) print } def

(dayflower) greeting            % => Hello, dayflower san!

スタックに値をつんで戻り値となすこともできます。関数ですね。

/BMI { exch 100 div dup mul div } def
160 50 BMI ==
% => 19.5312481
一時的な辞書を使って名前つきローカル変数もどき

さきほど説明した辞書スタックを使うと別の辞書において名前を束縛できます。これを利用すると手続き内でローカル変数をあつかうことができます。

/BMI {
  3 dict begin                  % サイズ3の一時的辞書を生成, カレント辞書に
    /weight exch def            % スタックトップの値を weight という名前に束縛
    /height exch def            % スタックトップの値を height という名前に束縛
    height 100 div
    dup mul /hgt2 exch def      % 身長の2乗を hgt2 という名前に束縛
    weight hgt2 div
  end
} def

/height 180 def         % デフォルトの辞書で束縛

160 50 BMI ==           % => 19.5312
height ==               % => 180 (壊れてない)

def オペレータは束縛する名前を +1 スタックからもってきますから、スタックトップの値を束縛するには exch を利用してスタックトップの2つを入れ替える必要がでてきます。

8. 制御文

選択

いわゆる if 文に相当するものです。

true  { (T) = } if                      % => (T)
false { (T) = } if                      % => -

true  { (T) } { (F) } ifelse ==         % => (T)
false { (T) } { (F) } ifelse ==         % => (F)

ifelse の連鎖をおこなうには、ifelse を多重にネストします。

false { (cond1) = } {
true  { (cond2) = } {
true  { (cond3) = } {
false { (cond4) = }
      { (else)  = }  ifelse } ifelse } ifelse } ifelse
% => cond2

さいごの ifelse } の連鎖がめんどくさいですけど。

繰り返し
% 回数指定繰り返し
5 { (hello) = } repeat
% => hello が5行表示される

% Basic 的 for
1 3 12 { == } for
% => 1, 4, 7, 10

% while 的なものは無限ループ loop と脱出 exit を使う
(V) (W) (X) (Y) (Z)
{ dup (X) eq { exit } if
  == } loop
% => (Z), (Y)
%    == でスタックトップから消化していることに注意

forall についてあとでかく)

9. 条件判断

真偽値は truefalse の2つしかありません((細かいことをいうと、真偽値たる値があって、デフォルトでシステム辞書に truefalse という名前が束縛されている。))。一般的な LL 言語のように数値など他のオブジェクトを真偽判定や論理演算には使えません。

関係オペレータ
1 1 eq ==               % => true  (==)
1 2 ne ==               % => true  (!=)

5 3 gt ==               % => true  (>)
2 4 lt ==               % => true  (<)
5 5 ge ==               % => true  (>=)
4 4 le ==               % => true  (<=)

% 文字列についても同じオペレータを使える
(abc) (abc) eq ==       % => true
(abc) (def) ne ==       % => true
(xyz) (abc) gt ==       % => true
(def) (ghi) lt ==       % => true
論理オペレータ
true true and ==        % => true
true false and ==       % => false

true false or ==        % => true
false false or ==       % => false

true not ==             % => false
false not ==            % => true

% XOR もあります
true true xor ==        % => false
false false xor ==      % => false
true false xor ==       % => true

10. ファイル入出力

ファイルコピーの例を示します。

/file_r (src.txt) (r) file def
/file_w (dst.txt) (w) file def
/buf 256 string def
{
  file_r buf readstring
  not { file_w exch writestring exit } if

  % 実際に読み込んだ値は buf より戻り値をみたほうがいい
  file_w exch writestring
} loop

file_w closefile
file_r closefile

11. 雑多なトピック

再帰

実行可能配列が評価されるのは実際に実行されるときなので、自身が束縛された名前をそのまま用いることができます*2。このため再帰は普通の LL 言語のように実現できます。

/fib {
  1 dict begin
    /n exch def
    {
      n 1 eq { 1 exit } if
      n 2 eq { 1 exit } if

      n 1 sub fib
      n 2 sub fib
      add
      exit
    } loop
  end
} def

1 fib ==        % => 1
3 fib ==        % => 2
5 fib ==        % => 5
10 fib ==       % => 55
高階手続き

(あとでかく)

クロージャ

ふつうにはクロージャは実現できません。がんばればできますが、分量的に最速マスターの範囲外になってしまいましたので補遺を書きました。

12. ページ記述言語

(あとでかく)

13. サンプル - Fizz Buzz

ここまでの知識があれば何か書けるよね。ってことで FizzBuzz を記述してみました。

%!PS-Adobe-3.0

/say {
  1 dict begin
    /val exch def
    val 15 mod 0 eq { (FizzBuzz) } {
    val  5 mod 0 eq { (Buzz)     } {
    val  3 mod 0 eq { (Fizz)     }
    {
      val
      100 string cvs
    } ifelse } ifelse } ifelse
    print
  end
} def

1 1 100 { say ( ) print } for
(\n) print

quit

14. おわりに

ざっくりと PostScript の文法を列挙しましたが、しくみはシンプル*3でありながら一方しっかりとした言語であるといえるのではないでしょうか。ただのページ記述言語であるにはとどまらず、極めるとPostScript でレイトレーシングをすることすらできます*4

参考文献

  • –³—¿ƒ`ƒ [ƒgƒŠƒAƒ‹FƒOƒ‰ƒtƒBƒbƒNƒX の「PostScript 実習マニュアル」
    • このテキストはほんとうにすごいです。この基礎文法最速マスターでよくわからなかった人はこちらを参照すれば一通りマスターできると思います。
  • Adobe PostScript language specifications の PostScript Language Reference, Third Edition
    • おおもとの仕様書ということでやはり参考になります。しかもどんなオペレータがあるか調べるのは一部(Operator Summary)だけ読めばいいし。実際には Third Edition より薄い PostScript Language Reference, Second Edition を参照しました。

追記

ご指摘ありがとうございます。

  • 2010-02-05 id:yshl さんのご指摘により、辞書スタックの項(store, def あたり)を書き直しました。

*1:ただし辞書スタックをさかのぼってもみつからなかった場合、カレント辞書に束縛します。

*2:とはいえ callee みたいなのは取得できないと思う。

*3:正直腰がひけていたのですが、文法やしくみ自体は思っていたよりコンパクトでした。

*4:hirax.net::17年前のプログラム・ミステリの解答編Tiny_RayTracing.ps を読んでみた。 - 大人が懐かしがることもない も参照してください。