桜技録

🐈🐈🐈🐈🐘

macOS 10.15向けにMozcをビルド

偉大な先駆者様。

環境

必要なもの

  • Ninja

  • Qt

いずれもHomebrewでインストール(Ninja 1.10.1 / Qt 5.15.1)。ビルド・スクリプトを動かすのに必要なPythonはOSプリインストールのもの(2.7.16)で問題なかった。

手順

基本は公式のHow to buildに従う。

リポジトリのclone

git clone https://github.com/google/mozc.git -b master --single-branch --recursive

cd mozc/src

以降の作業はcloneしたプロジェクト直下のsrcで行う。

ビルド

SDKとターゲットのバージョンを環境に合わせ、Qtのインストール場所をオプションで指定。

GYP_DEFINES="mac_sdk=10.15 mac_deployment_target=10.15" python build_mozc.py gyp --qtdir=/usr/local/opt/qt
python build_mozc.py build -c Release mac/mac.gyp:GoogleJapaneseInput gui/gui.gyp:config_dialog_main
python build_mozc.py build -c Release mac/mac.gyp:GoogleJapaneseInput mac/mac.gyp:gen_launchd_confs

が、下のエラーに遭遇。

/Users/sciencesakura/works/Clone/mozc/src/out_mac/Release/Breakpad/src/client/mac/sender/Breakpad.xib:global: error: Compiling for earlier than macOS 10.6 is no longer supported. [12]

エラー対処

コンパイル・ターゲットが古いということでBreakpad.xibを修正する。

  1. third_party/breakpad/src/client/mac/sender/Breakpad.xibXcodeで開く。エラーで指摘されたファイルとは異なるパスなので注意。

  2. 「Trust opening older file format?」と古くて安全でないフォーマットを新しいフォーマットに変換していいか訊かれるので「Open and Upgrade」で続ける。

  3. [Show the File inspector] > [Interface Builder Document] > [Document Editing] > [Build for] に「macOS 10.15 and Later」を指定して保存。

  4. Cleanして再ビルド。

インストール

成果物を所定の場所へ配置。

sudo cp -r out_mac/Release/Mozc.app /Library/Input\ Methods/
sudo cp out_mac/Release/gen/mac/org.mozc.inputmethod.Japanese.Converter.plist out_mac/Release/gen/mac/org.mozc.inputmethod.Japanese.Renderer.plist /Library/LaunchAgents

OSを再起動すると [System Preferences] > [Keyboard] > [Input Sources] でMozcが選択可能になる。

ノート: 情報科学における論理③

間が空いてしまったけれど小野寛晰『情報科学における論理』の読書ノート。なるべく練習問題も解いてく。(→前回)

2021-11-13 読み返してて気になる点があったので一部を訂正

第2章 述語論理

述語論理の論理式

言語

述語論理を記述するのに必要な以下の記号の集まりを言語(language)という。

  • 論理結合子 :  \land , \lor , \rightarrow , \lnot

  • 量化記号  :  \forall , \exists

  • 対象変数  : 議論対象の「もの」の集合を動く変数

  • 対象定数  : 議論対象の「もの」の集合のうち特定の要素を表す記号

  • 関数記号  : 議論対象の「もの」の集合上に定義された関数を表す記号

  • 述語記号  : 議論対象の「もの」の集合上に定義された述語を表す記号 

  • 補助記号  : 括弧やカンマなど

これらはあくまで記号であって、その記号が表す「もの」と同一視してはならない。関数  f と「  f 」という記号(文字)は別のものだと意識する。

  • 対象変数、対象定数は項(term)

  •  f  m 変数の関数記号、  t_i が項のとき、  f ( t_1 , \cdots , t_m ) は項

論理式
  •  P  n 変数の述語記号、  t_i が項のとき、  P ( t_1 , \cdots , t_n ) 論理式(formula)。この形を特に原子論理式(atomic formula)という

  •  A , B が論理式のとき、  A \land B , A \lor B , A \rightarrow B , \lnot A は論理式

  •  A が論理式、  x が対象変数のとき、  \forall x A , \exists x A は論理式

束縛変数と自由変数

論理式  \forall x A , \exists x A に於いて変数  x 出現(occurrence)を束縛された出現といい、  x束縛変数(bound variable)という。反対に束縛されていない出現を自由な出現といい、その変数を自由変数(free variable)という。

自由変数のない論理式を閉じた論理式(closed formula)といい、論理式  A のすべての自由変数  x_1 , \cdots , x_n を全称記号で束縛した論理式  \forall x_1 \cdots \forall x_n A  A 閉包(universal closure)という。

代入

論理式  A の自由変数  x を項  t で置き換えた論理式を  A [ t / x ] と表し、  x への  t 代入(substitution)という。特に複数回の代入を順序を設けず同時に実施する際は  A [ t_1 / x_1 , \cdots , t_n / x_n ] のように表す。

構造

言語  \mathscr{ L } に対して次の  U , I からなる2つ組を  \mathscr{ L } 構造(structure)という。

  •  U  \mathscr{ L } の対象変数が動く空でない集合。対象領域(domain)

  •  I  \mathscr{ L } の対象定数を  U の要素に、関数記号を  U 上の関数に、述語記号を  U 上の述語に対応させる写像解釈(interpretation)

解釈  I による像は  I ( x ) ではなく  x^{ I } と表記するのが一般的な模様。

論理式  A が構造  \mathfrak{ A } で正しいことを  \mathfrak { A } \vDash A で表す。

構造により「記号」と「記号が表す『もの』」とが関連付けられる。つまり記号列であった論理式に具体的な意味が生まれる。

言語  \mathscr{ L } とそれに対する構造  \mathfrak{ A } = ( U , I ) があるとき、  U の各要素  u に対応する各記号  \underline{ u }  \mathscr{ L } の対象定数に追加して新しい言語を定義できる。この言語を  \mathscr{ L } [ \mathfrak { A } ] で表す。

恒真な論理式

ある言語上の論理式  A が任意の構造に於いて正しいとき  A 恒真である(valid)といい、  \vDash A と表す。

練習問題

問2.1

1)-1 「ある学生が存在し、すべての教師が好きだ」すなわち「教師を好む学生がいる」

1)-2 「すべての怠け者はすべての教師が好きでない」すなわち「怠け者は教師を好きではない」

2)  \lnot \forall x ( T ( x ) \rightarrow L ( x ) )

問2.2

1) 「任意の実数  x , y について、  x \lt y ならば  x \lt z かつ  z \lt y を満たす実数  z が存在する」すなわち「実数は稠密である」

2) 「任意の実数  x , y , z について、  x \lt z かつ  z \lt y ならば  x \lt y 」すなわち「関係  \lt は推移律を満たす」

問2.3

 s  x が出現しない、かつ  t  y が出現しないこと

問2.4

等しいとはいえない。以下いずれをも満たす必要がある

  •  t  x が出現しないこと

  •  B_z  \forall z C または  \exists z C としたとき、  C に自由変数  x の出現する  B_y 、 自由変数  x の出現する  B_t 、 自由変数  y の出現する  B_t がいずれも  A の部分論理式でないこと

問2.5

1)-1 成り立たない。最大元は存在しない

1)-2 成り立つ。  x = a_4 のとき  y = a_4 とし、それ以外は  y = a_5 とすればよい

2)-3 成り立つ。  ( y , z ) = ( a_4 , a_5 ) のときなど

2)-4 成り立たない。  ( x , y , z ) = ( a_1 , a_2 , a_3 ) のときなど

問2.6

1)  \forall s \forall t ( ( A [ s / x ] \land A [ t / x ] ) \rightarrow s = t )

2)  \forall s \forall t ( ( A [ s / x ] \land A [ t / x ] ) \rightarrow s = t ) \land \exists s A [ s / x ]

3)  \exists s \exists t ( ( A [ s / x ] \land A [ t / x ] ) \land \lnot ( s = t ) )

問2.7

1)  \exists x \forall y ( x \leq y ) 自然数に最小元は存在するが整数には存在しない)

2)  \exists x \exists y ( \lnot ( y \leq x ) \land \forall z ( \lnot ( x \leq z ) \lor \lnot ( z \leq y ) ) ) (「間にどんな要素も挟まらない異なる2要素が存在する」の意。整数では正しいが有理数では正しくない)

問2.9

8) 構造  \mathfrak{ A } = ( \mathbf{ N } , I ) をとる。但し  D  x R y とし、  R^{ I } に大小関係  \gt をとる。このとき任意の自然数に対して真に大きい自然数は存在するが、すべての自然数より真に大きな自然数は存在しない。つまり  \mathfrak{ A } \vDash \forall y \exists x ( x R y ) かつ  \mathfrak{ A } \nvDash \exists x \forall y ( x R y ) 、従って  \mathfrak{ A } \nvDash \forall y \exists x ( x R y ) \rightarrow \exists x \forall y ( x R y )

9) 構造  ( \{ 0 , 1 \} , I ) をとればよい。但し  B  P ( x ) とし、  P^{ I } に「1である」をとる。

14) 構造  \mathfrak{ A } = ( \mathbf{ N } , I ) をとる。但し  ( B , C )  ( P ( x ) , Q ( x ) ) とし、  P^{ I } , Q^{ I } にそれぞれ「偶数である」、「奇数である」をとる。このとき明らかに  \mathfrak{ A } \nvDash \forall x P ( x ) 、よって  \mathfrak{ A } \vDash \forall x P ( x ) \rightarrow \forall x Q ( x ) 。一方これもまた明らかに  \mathfrak{ A } \nvDash \forall x ( P ( x ) \rightarrow Q ( x ) ) 。従って  \mathfrak{ A } \nvDash ( \forall x P ( x ) \rightarrow \forall x Q ( x ) ) \rightarrow \forall x ( P ( x ) \rightarrow Q ( x ) )

15) 構造  ( \{ 0 , 1 \} , I ) をとればよい。但し  ( B , C )  ( P ( x ) , Q ( x ) ) とし、  P^{ I } , Q^ { I } にそれぞれ「0または1である」、「1である」をとる。

問2.10

恒真でない。  \mathfrak{ A } \nvDash \exists x P ( x ) となるような構造  \mathfrak{ A } を示せばOK。

問2.11

1) 
  \exists x R ( x , y ) \rightarrow \forall y ( P ( y ) \land \lnot \forall z Q ( z ) ) \\
  \sim \forall x ( R ( x , y ) \rightarrow \forall y ( P ( y ) \land \lnot \forall z Q ( z ) ) ) \\
  \sim \forall x \forall t ( R ( x , y ) \rightarrow ( P ( t ) \land \lnot \forall z Q ( z ) ) ) \\
  \sim \forall x \forall t \exists z ( R ( x , y ) \rightarrow ( P ( t ) \land \lnot Q ( z ) ) )

2) 
  \exists x ( \forall y ( P ( y ) \rightarrow Q ( x , z ) ) \lor \exists z ( \lnot \exists u R ( z , u ) \land Q ( x , z ) ) ) \\
  \sim \exists x \forall y ( ( P ( y ) \rightarrow Q ( x , z ) ) \lor \exists z ( \lnot \exists u R ( z , u ) \land Q ( x , z ) ) ) \\
  \sim \exists x \forall y \exists t ( ( P ( y ) \rightarrow Q ( x , z ) ) \lor ( \lnot \exists u R ( t , u ) \land Q ( x , t ) ) ) \\
  \sim \exists x \forall y \exists t \forall u ( ( P ( y ) \rightarrow Q ( x , z ) ) \lor ( \lnot R ( t , u ) \land Q ( x , t ) ) )

(→次回)

Gradle+JaCoCoでカバレッジ・レポートから除外したいファイルを指定する

jacocoTestReport {
  afterEvaluate {
    classDirectories.setFrom(classDirectories.files.collect {
      fileTree(dir: it, excludes: ["**/xxx/*", "**/Yyy.class"])
    })
  }
}

前は classDirectories にフィルタしたファイルコレクションを直接代入していたけれどGradle 6.0より読取専用になっているので代わりに setFrom メソッドを使う。

GradleからTomcat Manager Appを操作してwarをデプロイする

Gradleを使ってwarファイルをTomcatにデプロイしたいという場面に於いて、warファイルを$CATALINA_BASE/webappsにコピーする手法をよく見かけるが、それとは異なる手法としてTomcatにデフォルトで入っているManager AppをGradleから使う術を記しておく。

warファイルのコピーは単純だがGradleから見てコピー先がローカルなのかネットワーク上なのかDockerコンテナ内なのかを意識する必要がある。一方HTTPクライアントからManager Appを叩く分にはそのような環境差異を意識する必要はなく複数の異なる環境でもビルド・スクリプトを共有しやすい。

前準備

まずはManager AppをGUIレスで使えるようにする。

Tomcatをインストールした直後はManager Appを始めとしたプリインストールのアプリは$CATALINA_BASE/webapps.distにありデフォルトで無効になっている。これを$CATALINA_BASE/webappsから見えるようにする。

下はシンボリック・リンクを用いた例。

ln -s \
  $CATALINA_BASE/webapps.dist/manager \
  $CATALINA_BASE/webapps/

続いて$CATALINA_BASE/conf/tomcat-user.xmlを編集しmanager-scriptロールを持つユーザを作成する。

<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
  <role rolename="manager-script"/>
  <user username="USERNAME" password="PASSWORD" roles="manager-script"/>
</tomcat-users>

WebブラウザからGUIでManager Appを使う為のmanager-guiというロールもあるが同一ユーザにmanager-guiとmanager-scriptを一緒に持たせないことが推奨されている*1。(もしGUIも使うのであれば)GUI用とスクリプト用でユーザを分けるのが良いだろう。

最後にManager Appへの接続を許可するアクセス元アドレスの範囲を必要に応じて$CATALINA_BASE/conf/[enginename]/[hostname]/manager.xmlに設定する。

デフォルトの設定は$CATALINA_BASE/webapps.dist/manager/META-INF/context.xmlにありループバック・アドレスからのみを許可している。ネットワーク上のサーバであれば管理セグメントの特定ノードからは許可する等といった変更がいるだろう。

下は192.168.1.*からのアクセスを許可する例。

<Context privileged="true" antiResourceLocking="false">
   <Valve className="org.apache.catalina.valves.RemoteAddrValve" 
          allow="192\.168\.1\.\d+" />
</Context>

以上の設定を済ましてTomcatを再起動すれば作成したユーザでリモート・デプロイが可能になる。

Gradleでデプロイ

下例ではHTTPクライアントとして個人的にGradleで使いやすいと感じているgradle-http-pluginでdeployタスクを定義してみた。

USERNAMEPASSWORDは上で作成したmanager-scriptユーザのもの。外部定義して環境毎にURLとユーザを切り替えれば複数環境でタスクを共有出来る。

import groovyx.net.http.NativeHandlers
import io.github.httpbuilderng.http.HttpTask

plugins {
  id 'war'
  id 'io.github.http-builder-ng.http-plugin' version '0.1.1'
}

task deploy(type: HttpTask, dependsOn: 'war') {
  config {
    request.uri = 'http://example.com:8080'
    request.auth.basic('USERNAME', 'PASSWORD')
    request.encoder('application/octet-stream', NativeHandlers.Encoders.&handleRawUpload)
  }
  put {
    request.uri.path = '/manager/text/deploy'
    request.uri.query = [path: "/${war.archiveBaseName.get()}", update: true]
    request.contentType = 'application/octet-stream'
    request.body = war.archiveFile.get().asFile
    response.success { fs, body ->
      def message = body as String
      println(message)
      if (message.startsWith('FAIL')) throw new Exception(message)
    }
  }
}

ポイントとなるのはrequest.uri.queryに渡しているクエリパラメータ。

  1. path:対象アプリのコンテキストパスを/myappのようにスラッシュ始まりで指定する。GUIのManager Appやwar配置と違って省略することは出来ない。ROOTアプリケーションの場合は単に/を指定する。ここでは/ + warファイルのベース名を指定している。

  2. update:pathで指定したコンテキストパスにアプリがすでにあった場合の動作を制御する。false(デフォルト値)ならエラーとし、trueならば既存アプリをアンデプロイした後warをデプロイする。繰り返し使用する使途ではtrue固定で良いだろう。

その他に指定可能なパラメータやデプロイ以外に用意されているAPI(アンデプロイやアプリの一覧取得)は公式マニュアルが詳しい。

おまけ:curlでデプロイ

curlでも十分戦える。

curl --basic --user USERNAME:PASSWORD \
  --upload-file build/libs/myapp.war \
  'http://example.com:8080/manager/text/deploy?path=/myapp&update=true'

Maven CentralのRepository Writableエラー

Gradle 6.0.1のMaven Publishプラグインを使ってパッケージをMaven Centralへリリースしようとしたところ以下のエラーで失敗してしまった。

f:id:sciencesakura:20200329212120p:plain

Event: Failed: Repository Writable

typeId         RepositoryWritePolicy
failureMessage Artifact updating: Repository ='releases:Releases' does not allow updating artifact='/com/sciencesakura/dbsetup-spreadsheet/maven-metadata.xml.sha512'
failureMessage Artifact updating: Repository ='releases:Releases' does not allow updating artifact='/com/sciencesakura/dbsetup-spreadsheet/maven-metadata.xml.sha256'

探したところ既にissueが立っていた。

https://issues.sonatype.org/browse/MVNCENTRAL-5276

https://issues.sonatype.org/browse/NEXUS-21802

チェックサムに使用するハッシュ関数としてSHA-256とSHA-512がGradle 6.0で追加されたのだが、NexusMaven Centralのリポジトリ・マネージャ)側にmaven-metadata.xmlのSHA-256/SHA-512チェックサムがあるとリリースに失敗する問題があったということらしい(2020年3月現在未解決)。

暫定的な回避策は2つ提示されている。

回避策①:チェックサムを手動削除

Staging repositoryでclose処理する前にアップロードしたファイルの中からmaven-metadata.xml.sha256maven-metadata.xml.sha512を消す。

試していないがNexus管理画面のContentタブでアップロードしたファイルの一覧が閲覧出来るのでおそらくそこで削除が出来るのだろう。

回避策②:チェックサムを無視するフラグを有効化

Gradle 6.0.1で追加されたシステム・プロパティorg.gradle.internal.publish.checksums.insecuretrueを設定する。これによりSHA-256/SHA-512のチェックサムがアップロードされなくなる。

プロジェクト直下のgradle.propertiesに次の行を加えると良い。

systemProp.org.gradle.internal.publish.checksums.insecure=true

dbsetup-csv: DbSetupでCSVデータを投入する

Javaユニットテストにて自動でテストデータをデータベースへ投入する為のツールとしてはDbSetupDBUnitをしばしば使っている。

ここ数年はコードの中でそのテストケースの確認に必要なデータを明示的に記述できるDbSetupの方を推しているのだが、カラム数の多いテーブルにデータをセットする場合は可読性が頗る悪くなってしまいテストデータをCSVExcelに記載するDBUnitが懐かしくなる。

とはいえ2つのツールを1つのプロジェクトに詰めるのも気持ち悪く、CSVを読んでDbSetupのInsertオペレーションにマップするスニペットを書いて使いまわしていたのだが流石にコピペを繰り返すのが阿呆臭くなってきたのでリライトしてパッケージとして公開することにした。

github.com

インストール

Maven Centralで公開中。GradleとMavenの例。

dependencies {
  testImplementation 'com.ninja-squad:DbSetup:2.1.0' 
  testImplementation 'com.sciencesakura:dbsetup-csv:0.0.1'
}
<dependency>
  <groupId>com.ninja-squad</groupId>
  <artifactId>DbSetup</artifactId>
  <version>2.1.0</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>com.sciencesakura</groupId>
  <artifactId>dbsetup-csv</artifactId>
  <version>0.0.1</version>
  <scope>test</scope>
</dependency>

使い方

投入するデータファイルはクラスパス配下となる場所に置くこと。GradleやMavenを使っているのなら通常 src/test/resources より下に置く。

以下は m_user テーブルをtruncateして src/test/resources/data/m_user.csv を同テーブルにインポートする例。

import com.ninja_squad.dbsetup.DbSetup;
import com.ninja_squad.dbsetup.DbSetupTracker;
import com.ninja_squad.dbsetup.destination.Destination;
import com.ninja_squad.dbsetup.operation.Operation;
import org.junit.jupiter.api.BeforeEach;

import static com.ninja_squad.dbsetup.Operations.sequenceOf;
import static com.ninja_squad.dbsetup.Operations.truncate;
import static com.sciencesakura.dbsetup.csv.Import.csv;

class Sample {

    private static final DbSetupTracker dbSetupTracker = new DbSetupTracker();

    private Destination destination;

    @BeforeEach
    void setUp() {
        Operation operation = sequenceOf(
            truncate("m_user"),
            csv("data/m_user.csv").into("m_user").build()
        );
        DbSetup dbSetup = new DbSetup(destination, operation);
        dbSetupTracker.launchIfNecessary(dbSetup);
    }
}

特に何も指定しなければデータファイルはUTF-8でカンマ区切り、フィールドの囲み文字*1は二重引用符、最初の1行はヘッダと見做され、空文字はnullと評価される。

これらは以下のようにカスタマイズ可能。

csv("data/m_user.csv").into("m_user")
    // エンコーディングを指定
    .withCharset("ms932")
    // 区切り文字を指定
    .withDelimiter('\t')
    // 囲み文字を指定
    .withQuote('\'')
    // ヘッダを指定 (ファイルの1行目からデータとして扱う)
    .withHeader("id", "user_no", "user_name", "email_address", "tel_no")
    // nullと評価する文字列を指定
    .withNullAs("[null]")
    .build();

なるべく生のDbSetupで

とまあ自作のツールを紹介したけれども初めに書いた通りDbSetupを使う良さはテストコードとテストデータを近い位置に記述させることだと考えているので、それを壊し得るこのツールの使い所はなるべく絞っていきたい。

目安としては生の Operations#insertInto を使って1レコード分の values(String...) に折り返しが発生してしまうようならこのツールの出番だろうか。

【追記】Excel版も作った。

github.com

*1:区切り文字を含む値や空白のみの値を表す場合は必須、それ以外の場合はオプション

ノート: 情報科学における論理②

小野寛晰『情報科学における論理』の読書ノート。(→前回)

第1章 命題論理(承前)

今回はシークエント計算を実装してみる。

定理1.8の証明で登場した式の分解を推論規則に、完全分解木を証明図に流用したいので、形式体系LKそのままではなくそれを少し変形した形式体系LK'を新たに定義して実装する。

形式体系LK'

用意した推論規則は次の8つ。ラテン文字は論理式、ギリシア文字は長さ0以上の論理式の列とする。

∧ Left (∧L)
 \cfrac { \Gamma_1 , A , B , \Gamma_2 \vdash \Delta } { \Gamma_1 , A \land B , \Gamma_2 \vdash \Delta }
∧ Right (∧R)
 \cfrac { \Gamma \vdash \Delta_1 , A , \Delta_2 \ \ \ \  \Gamma \vdash \Delta_1 , B , \Delta_2 } { \Gamma \vdash \Delta_1 , A \land B , \Delta_2 }
∨ Left (∨L)
 \cfrac { \Gamma_1 , A , \Gamma_2 \vdash \Delta \ \ \ \  \Gamma_1 , B , \Gamma_2 \vdash \Delta } { \Gamma_1 , A \lor B , \Gamma_2 \vdash \Delta }
∨ Right (∨R)
 \cfrac { \Gamma \vdash \Delta_1 , A , B , \Delta_2 } { \Gamma \vdash \Delta_1 , A \lor B , \Delta_2 }
→ Left (→L)
 \cfrac { \Gamma_1 , \Gamma_2 \vdash \Delta , A \ \ \ \  \Gamma_1 , B , \Gamma_2 \vdash \Delta } { \Gamma_1 , A \rightarrow B , \Gamma_2  \vdash \Delta }
→ Right (→R)
 \cfrac { A , \Gamma \vdash \Delta_1 , B , \Delta_2 } { \Gamma \vdash \Delta_1 , A \rightarrow B , \Delta_2 }
¬ Left (¬L)
 \cfrac { \Gamma_1 , \Gamma_2 \vdash \Delta , A } { \Gamma_1 , \lnot A , \Gamma_2 \vdash \Delta }
¬ Right (¬R)
 \cfrac { A , \Gamma \vdash \Delta_1 , \Delta_2 } { \Gamma \vdash \Delta_1 , \lnot A , \Delta_2 }

始式には  \Gamma_1 , A , \Gamma_2 \vdash \Delta_1 , A , \Delta_2 を採用する。

実装

TypeScriptで実装。ソース全体は↓。

github.com

まずは論理式、式、証明図といったモデルたち。

/**
 * Represents a logical formula.
 */
interface Formula {
    /** Gets the identifier if this formula is a propositional variable, `undefined` otherwise. */
    readonly identifier?: string;

    /** Gets the operator if this formula is a compound formula, `undefined` otherwise. */
    readonly operator?: Operator;

    /** Gets the first operand if this formula is a compound formula, `undefined` otherwise. */
    readonly operand1?: Formula;

    /** Gets the second operand if this formula is a compound formula and the operator is binary, `undefined` otherwise. */
    readonly operand2?: Formula;
}

/**
 * Represents a sequence of zero or more formulas.
 */
type Formulas = ReadonlyArray<Formula>;

/**
 * Represents a sequent.
 */
interface Sequent {
    /** Gets the formula sequence for antecedents. */
    readonly antecedents: Formulas;

    /** Gets the formula sequence for succedents. */
    readonly succedents: Formulas;
}

/**
 * Represents a proof figure as tree structure.
 */
interface ProofTree {
    /** Gets the height of this tree. */
    readonly height: number;

    /** Gets the root node of this tree. */
    readonly root: ProofTreeNode;
}

/**
 * Represents a node of `ProofTree`.
 */
interface ProofTreeNode {
    /** Gets the level of this node. */
    readonly level: number;

    /** Gets the sequent. */
    readonly sequent: Sequent;

    /** Gets the reference to left child node. */
    readonly left?: ProofTreeNode;

    /** Gets the reference to right child node. */
    readonly right?: ProofTreeNode;
}

そして推論規則。式(=上式)を引数に長さ1または2の式のタプル(=下式)を返す関数として表現した。ここでは∧Lと∧Rを抜萃。

/**
 * Represents a inference rule.
 */
type InferenceRule = (sequent: Sequent, position: number) => [Sequent, Sequent?];

/**
 * The inference rule that means `&&L`.
 */
const andL: InferenceRule = (s: Sequent, position: number) => {
    const principal = s.antecedents[position];
    const left = s.antecedents.slice(0, position);
    const right = s.antecedents.slice(position + 1);
    return [sequent([...left, principal.operand1!, principal.operand2!, ...right], s.succedents)];
};

/**
 * The inference rule that means `&&R`.
 */
const andR: InferenceRule = (s: Sequent, position: number) => {
    const principal = s.succedents[position];
    const left = s.succedents.slice(0, position);
    const right = s.succedents.slice(position + 1);
    return [
        sequent(s.antecedents, [...left, principal.operand1!, ...right]),
        sequent(s.antecedents, [...left, principal.operand2!, ...right])
    ];
};

推論規則を使って論理結合子がなくなるまで式の分解を繰り返す完全分解木(証明図)生成ゾーン。完全分解木の葉がすべて始式であれば与えられた式は証明可能、すなわちトートロジーであると判断する。

/**
 * Checks whether the specified sequent is an initial sequent.
 *
 * @param the sequent to check
 * @return `true` if the specified sequent is an initial sequent, `false` otherwise
 */
const isInitial: (sequence: Sequent) => boolean = (sequent: Sequent) => {
    for (const i of sequent.antecedents) {
        for (const j of sequent.succedents) {
            if (formequ(i, j)) return true; // formequは論理式の同値関係
        }
    }
    return false;
};

/**
 * Decomposes the specified sequent into one or two sequents.
 *
 * @param sequent the sequent to decompose
 * @return one or two decomposed sequents if possible, `null` otherwise
 */
const decompose: (sequent: Sequent) => [Sequent, Sequent?] | null = (sequent: Sequent) => {
    let position = sequent.antecedents.findIndex(isCompound); // isCompoundは論理式が論理結合子を含んでいるかの述語
    let rule: InferenceRule;
    if (position === -1) {
        position = sequent.succedents.findIndex(isCompound);
        if (position === -1) return null;
        rule = rightRule(sequent.succedents[position].operator!);
    } else {
        rule = leftRule(sequent.antecedents[position].operator!);
    }
    return rule(sequent, position);
};

/**
 * Proves the specified sequent.
 *
 * @param sequent the sequent to prove
 * @return the proof result
 */
const prove: (sequent: Sequent) => Proof = (sequent: Sequent) => {
    let provable = true;
    let maxLv = 0;
    const makeTree: (s: Sequent, level: number) => ProofTreeNode = (s: Sequent, level: number) => {
        if (!provable) return { level, sequent: s };
        const decomposed = decompose(s);
        if (!decomposed) {
            provable = provable && isInitial(s);
            maxLv = Math.max(maxLv, level);
            return { level, sequent: s };
        }
        if (decomposed[1]) {
            return {
                level,
                sequent: s,
                left: makeTree(decomposed[0], level + 1),
                right: makeTree(decomposed[1], level + 1)
            };
        } else {
            return {
                level,
                sequent: s,
                left: makeTree(decomposed[0], level + 1)
            };
        }
    };
    const root = makeTree(sequent, 0);
    if (provable) {
        return {
            provable,
            figure: {
                height: maxLv + 1,
                root
            }
        };
    } else {
        return { provable };
    }
};

動かすとこんな感じ。例はド・モルガンの定理。

f:id:sciencesakura:20190930000131p:plain

(→次回)