はじめに
アドベントカレンダーの内容に困ったのでMSBuildでBrainf**kの実装をすることにしました(?)(先日MSBuildについて少し調べる機会があったのと、そういえばBrainf**kの実装をしたことがなかったなというのを思ったのとで)
MSBuildでは再帰実行ができますし、C#の文字列関数もほとんどそのまま使えるのでそれほど難しくありません。
MSBuildとは
MSBuildは、Microsoftが提供している、XMLベースで記述するビルドエンジンです。
詳しくは公式ドキュメントを参照してください。
https://docs.microsoft.com/ja-jp/visualstudio/msbuild/msbuild?view=vs-2017
身近なところだとVisual Studioで生成される.csproj
ファイルもこのMSBuildファイルになっています。
Brainf**kとは
8つの命令だけからなる非常にシンプルなプログラミング言語です。
https://ja.wikipedia.org/wiki/Brainfuck
なお今回はBrainf**kの命令のうち「,」(C言語のgetchar()に相当)は対応していません。
前提
バージョン
今回、動作確認は下記のバージョンのMSBuildを使用しました。
.NET Framework 向け Microsoft (R) Build Engine バージョン 15.9.21+g9802d43bc3
後で解説しますが今回はドキュメントに記載されてない挙動を用いた実装をいくらかやっているので別のバージョンだと動かないかもしれません。
制約
MSBuildはC#コードの埋め込みができるなど非常に自由度の高いビルドエンジンのため、制約なしで実装すればとても簡単にBrainf**kを実装できてしまいます。それだと面白くないので今回はいくつかの制約を設けることにしました。
タスクの使用制限
MSBuildにおいてタスクとはなんらかのアクションを実行するための機能です。
タスクには最初から用意されているものと、自分で定義して使用するものとがありますが、今回は下記の2つのタスクのみを使用可能なものとしました。
- MSBuildタスク(ただしProjects属性に指定できるのは自身のファイルのみ)
- Messageタスク
MSBuildタスクとMessageタスクはそれぞれ再帰処理とメッセージ出力に必要となります。
他のタスクの使用を許可するとC#ファイルのコンパイルができてしまったり、C#コードの埋め込みができてしまったりするので、Brainf**k実装の難易度は大きく下がるものと思われます。
プロパティ関数/アイテム関数の使用制限
MSBuildにはC#などの関数を使用できるプロパティ関数/アイテム関数という機能があります。
今回はこれらの関数のうち、次のもののみを使用可能なものとします。
- stringクラスのメソッド
- Convertクラスのメソッド(型変換に使用)
-
[MSBuild]::
の記述で使用できるAdd
やSubtract
などの計算メソッド
他の関数の使用を許可した場合にBrainf**k実装がどれだけ簡単になるかは精査しておらず不明なのですが、対象の関数があまりにも多く、その影響の精査が困難なためとりあえず上記のものに絞った次第です。
インポートの禁止
外部のファイルをインポートするのも今回は禁止です。
実装
実装コード
早速、実装したコードです。後ろに解説があります。
だいぶ無理やり実装しています。
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" TreatAsLocalProperty="Input;Pointer;ProgramCounter;Memory;LoopStack;LoopSkipDepth">
<!-- ユーザーが実行した直後の時(=再帰から呼び出されたのではない時) -->
<PropertyGroup Condition="'$(Recursive)' == ''">
<Input>$(Input.Trim())</Input>
<ProgramCounter>%00</ProgramCounter>
<Pointer>%00</Pointer>
<LoopSkipDepth>%00</LoopSkipDepth>
</PropertyGroup>
<!-- 再帰から呼び出された時 -->
<PropertyGroup Condition="'$(Recursive)' != ''">
<ProgramCounter>$(ProgramCounter.Substring(1, $([MSBuild]::Subtract($(ProgramCounter.Length), 2))))</ProgramCounter>
<Pointer>$(Pointer.Substring(1, $([MSBuild]::Subtract($(Pointer.Length), 2))))</Pointer>
<Memory>$(Memory.Substring(1, $([MSBuild]::Subtract($(Memory.Length), 2))))</Memory>
<LoopStack>$(LoopStack.Substring(1, $([MSBuild]::Subtract($(LoopStack.Length), 2))))</LoopStack>
<LoopSkipDepth>$(LoopSkipDepth.Substring(1, $([MSBuild]::Subtract($(LoopSkipDepth.Length), 2))))</LoopSkipDepth>
</PropertyGroup>
<PropertyGroup>
<Memory>$(Memory.PadRight($([MSBuild]::Add($(Pointer[0]), 1)), %00))</Memory>
</PropertyGroup>
<Target Name="Main" Condition="$([MSBuild]::Subtract($(ProgramCounter[0]), $(Input.Length))) < 0">
<PropertyGroup>
<Instruction>$(Input.Substring($(ProgramCounter[0]), 1))</Instruction>
</PropertyGroup>
<Message Condition="'$(Debug)' == '1'" Text="Before: ProgramCounter=$(ProgramCounter); Pointer=$(Pointer); Memory=$(Memory); LoopStack=$(LoopStack); LoopSkipDepth=$(LoopSkipDepth)" Importance="high" />
<Message Condition="'$(Debug)' == '1'" Text="Instruction=$(Instruction)" Importance="high" />
<PropertyGroup>
<PointerIncrement>0</PointerIncrement>
<MemoryIncrement>0</MemoryIncrement>
<LoopStart>0</LoopStart>
<LoopEnd>0</LoopEnd>
<OutputString></OutputString>
<PointerIncrement Condition="'$(Instruction)' == '>'">1</PointerIncrement>
<PointerIncrement Condition="'$(Instruction)' == '<'">-1</PointerIncrement>
<MemoryIncrement Condition="'$(Instruction)' == '+'">1</MemoryIncrement>
<MemoryIncrement Condition="'$(Instruction)' == '-'">-1</MemoryIncrement>
<LoopStart Condition="'$(Instruction)' == '['">1</LoopStart>
<LoopEnd Condition="'$(Instruction)' == ']'">1</LoopEnd>
<OutputString Condition="'$(Instruction)' == '.'">$(Memory.get_Chars($(Pointer[0])))</OutputString>
</PropertyGroup>
<PropertyGroup Condition="'$(PointerIncrement)' != '0' And '$(LoopSkipDepth)' == '%00'">
<Pointer>$([System.Convert]::ToChar($([MSBuild]::Add($(Pointer[0]), $(PointerIncrement)))))</Pointer>
</PropertyGroup>
<PropertyGroup Condition="'$(MemoryIncrement)' != '0' And '$(LoopSkipDepth)' == '%00'">
<MemoryBeforePointer>$(Memory.Substring(0, $(Pointer[0])))</MemoryBeforePointer>
<MemoryOnPointer>$([System.Convert]::ToChar($([MSBuild]::Add($(Memory.get_Chars($(Pointer[0]))), $(MemoryIncrement)))))</MemoryOnPointer>
<MemoryAfterPointer>$(Memory.Substring($([MSBuild]::Add($(Pointer[0]), 1))))</MemoryAfterPointer>
<Memory>$(MemoryBeforePointer)$(MemoryOnPointer)$(MemoryAfterPointer)</Memory>
</PropertyGroup>
<PropertyGroup Condition="'$(LoopStart)' != '0'">
<LoopStack>$(LoopStack)$(ProgramCounter)</LoopStack>
<LoopSkipDepth Condition="'$(LoopSkipDepth)' == '%00' And '$(Memory.get_Chars($(Pointer[0])))' == '%00'">$([System.Convert]::ToChar($(LoopStack.Length)))</LoopSkipDepth>
</PropertyGroup>
<PropertyGroup Condition="'$(LoopEnd)' != '0' And '$(Memory.get_Chars($(Pointer[0])))' != '%00'">
<LoopStackLastIndex>$([MSBuild]::Subtract($(LoopStack.Length), 1))</LoopStackLastIndex>
<ProgramCounter>$(LoopStack[$(LoopStackLastIndex)])</ProgramCounter>
</PropertyGroup>
<PropertyGroup Condition="'$(LoopEnd)' != '0' And '$(Memory.get_Chars($(Pointer[0])))' == '%00'">
<LoopSkipDepth Condition="'$([MSBuild]::Subtract($(LoopSkipDepth[0]), $(LoopStack.Length)))' == '0'">%00</LoopSkipDepth>
<LoopStack>$(LoopStack.Substring(0, $([MSBuild]::Subtract($(LoopStack.Length), 1))))</LoopStack>
</PropertyGroup>
<Message Condition="'$(OutputString)' != '' And '$(LoopSkipDepth)' == '%00'" Text="$(OutputString)" Importance="high" />
<Message Condition="'$(Debug)' == '1'" Text="After: ProgramCounter=$(ProgramCounter); Pointer=$(Pointer); Memory=$(Memory); LoopStack=$(LoopStack); LoopSkipDepth=$(LoopSkipDepth)" Importance="high" />
<MSBuild Projects="$(MSBuildProjectFullPath)" Targets="Main" Properties="Recursive=1; Input=$(Input); ProgramCounter='$([System.Convert]::ToChar($([MSBuild]::Add($(ProgramCounter[0]), 1))))'; Pointer='$(Pointer)'; Memory='$(Memory)'; LoopStack='$(LoopStack)'; LoopSkipDepth='$(LoopSkipDepth)'" />
</Target>
</Project>
全体の要点の解説
まずは全体の作りを理解するための要点から解説していきます。MSBuildやBrainf**kの基本的な部分については解説したりしなかったりしていますので、わからない概念があれば先に示したドキュメントを参照しながら読み進めていただけるとよいかもしれません。
Brainf**kのプログラムコードの受け取り
今回、Brainf**kのプログラムコードはInput
というプロパティで受け取っています。
実行状態の持ち方
今回のプログラムでは、実行状態として次の5つのデータを保持しています。
-
ProgramCounter
: プログラムの実行位置を保持しておくためのプロパティ -
Pointer
: ポインタ位置を保持しておくためのプロパティ -
Memory
: メモリデータを保持しておくためのプロパティ -
LoopStack
: ループ開始位置([の位置)を保持しておくためのプロパティ -
LoopSkipDepth
: ループを1度も実行せずスキップする場合に使用するプロパティ
これらの状態の保持に使えそうな仕組みとして、MSBuildにはプロパティとアイテムという変数のようなものがあります。
ざっくり、プロパティは文字列変数のようなもの、アイテムは(かなり癖のある)配列のようなものです。
Memory
やLoopStack
は配列状になるためアイテムを使いたいところなのですが、アイテムだと指定番目の要素を取り出すことができないなど、今回の用途だと使い勝手が良いとは言い難いです。
そのため今回はすべての実行状態をアイテムではなくプロパティで保持することにします。
ProgramCounterやMemoryはすべて数値データとなりますが、プロパティを使う場合はすべてを文字列として保持・受け渡しをすることになるので、各数値は文字コードとして文字列に変換して持っておくことにします。
たとえばProgramCounter
が65
の時、これは65
の文字コードを持つA
という文字列として表します。(参考: ASCIIコード表)
たとえばMemory
が[65, 68, 67]
の時、これはADC
という文字列として表します。
他に数値を数値文字列として(1という数値を1という文字列として)扱うことも考えましたが、MemoryやLoopStackの表現が少し辛かったので今回はやめておきました。
プログラムコードの読み込みと再帰
プログラムコードの読み込みは再帰を使って行います。
実装の一番下の方にある次の個所が再帰を行っているところです。
<MSBuild
Projects="$(MSBuildProjectFullPath)"
Targets="Main"
Properties="Recursive=1; Input=$(Input); ProgramCounter='$([System.Convert]::ToChar($([MSBuild]::Add($(ProgramCounter[0]), 1))))'; Pointer='$(Pointer)'; Memory='$(Memory)'; LoopStack='$(LoopStack)'; LoopSkipDepth='$(LoopSkipDepth)'" />
<MSBuild>
は任意のプロジェクトの任意のターゲットを実行するタスクです。
$(MSBuildProjectFullPath)
はこのファイル自身のパスが入ったプロパティです。
この場合は<MSBuild>
タスクを用いてこのファイル自身のMain
というターゲットを実行することを意味しています。
Main
というのはまさにこのコードが含まれているタスクです(コードの上の方の<Target Name="Main" ...
という部分を参照)。
これによって再帰を行い、ProgramCounterの値を進めつつプログラムコードを読み込んでいます。
ループ
今回はBrainf**kの[の命令を読み込む度に(ループが始まる度に)その位置をLoopStackの末尾に追加しています。
またBrainf**kでは、[の命令が読み込まれた時点でポインタの示すメモリのデータが0の場合は、そのループを1度も実行せずにスキップすることになっています。これを実現するため、その場合にはLoopSkipDepthにその時点でのLoopStackの長さを記録しておき、その記録がある間は各処理をスキップします。
各コードの詳細解説
以下、各コードの詳細な解説をしていきます。なお、解説の途中でMSBuildコードと同等の動作をするC#風のコードをいくつか示していますが、やってることの雰囲気を伝えることを優先した都合上、型変換の処理や、自明な配列のインデクサなどを一部省略しています。
実行状態の初期化
<PropertyGroup Condition="'$(Recursive)' == ''">
<Input>$(Input.Trim())</Input>
<ProgramCounter Condition="'$(ProgramCounter)' == ''">%00</ProgramCounter>
<Pointer Condition="'$(Pointer)' == ''">%00</Pointer>
<LoopSkipDepth Condition="'$(LoopSkipDepth)' == ''">%00</LoopSkipDepth>
</PropertyGroup>
これはC#風のコードで表すと下記のようなものになります。
if (!recursive)
{
programCounter = 0;
pointer = 0;
loopSkipDepth = 0;
}
Recursiveプロパティは再帰によって実行された時のみ値が設定されるよう実装しています。このRecursiveプロパティが空の時とは、すなわち、まだ1度も再帰を行っていない、ユーザーがコマンドから実行した直後の時です。
その場合に上記のように実行状態を初期化しています。
ちなみに%xx
というのはMSBuildでASCIIコードから文字を生成するためのものです(参考)
先に書いたように今回は各状態値を文字コードとした文字列にシリアライズして保持しているので、ここではASCIIコードで0の文字(ヌル文字)を代入しています。
再帰で受け取ったプロパティからシングルクォーテーションを削除
<PropertyGroup Condition="'$(Recursive)' != ''">
<ProgramCounter>$(ProgramCounter.Substring(1, $([MSBuild]::Subtract($(ProgramCounter.Length), 2))))</ProgramCounter>
<Pointer>$(Pointer.Substring(1, $([MSBuild]::Subtract($(Pointer.Length), 2))))</Pointer>
<Memory>$(Memory.Substring(1, $([MSBuild]::Subtract($(Memory.Length), 2))))</Memory>
<LoopStack>$(LoopStack.Substring(1, $([MSBuild]::Subtract($(LoopStack.Length), 2))))</LoopStack>
<LoopSkipDepth>$(LoopSkipDepth.Substring(1, $([MSBuild]::Subtract($(LoopSkipDepth.Length), 2))))</LoopSkipDepth>
</PropertyGroup>
これはC#風のコードで表すと下記のようなものになります。
if (recursive)
{
programCounter = programCounter.Substring(1, programCounter.Length - 2);
pointer = pointer.Substring(1, pointer.Length - 2);
memory = memory.Substring(1, memory.Length - 2);
loopStack = loopStack.Substring(1, loopStack.Length - 2);
loopSkipDepth = loopSkipDepth.Substring(1, loopSkipDepth.Length - 2);
}
いきなりわけのわからないコードが出てきました。
これは、再帰でプロパティを受け渡す際にどうしても値の前後にシングルクォーテーションがついてしまうため、それを除去するコードです。
先に書いたように実行状態はその値を文字コードとする文字列に変換して保持していますが、これは<MSBuild>
タスクでの受け渡しの際に問題になります。
というのも、たとえばMemoryが下記のような状態の場合を考えます。
Memory[0] = 32
Memory[1] = 97
Memory[2] = 98
この場合、<MSBuild>
で再帰を行う際に次のようにMemoryを受け渡そうとすると…
Properties="...; Memory=$(Memory); ...
…次のように展開されてしまいます。(ASCIIコードにおいて32は半角スペース、97はa、98はbをそれぞれ表すため)
Properties="...; Memory= ab; ..."
そして=
の直後の半角スペースは無視をされてしまうので、実行先にMemoryを" ab"
として渡したかったのに、実際に渡すのは先頭の半角が削られた"ab"
という値になっています。
これについては単純に値を''(シングルクォーテーション)で囲えば回避できます。
Properties="...; Memory='$(Memory)'; ..."
Properties="...; Memory=' ab'; ..."
ただこれはこれで別の問題があり、MSBuildの仕様なのかバグなのか判然としないのですが、値を''で囲むと実行先で受け取る値にも''が含まれてしまいます。
つまり今の例の場合で言えば、" ab"
という値を渡したかったのに、"' ab'"
として受け取られてしまうということです。
この問題に対処するため、先に示したコードのように先頭・末尾からシングルクォーテーションを削除しています。
もし先のバージョンのMSBuildでこの問題がなおる場合、このコードは削除が必要になります。
Pointerの示す位置までMemoryを拡張
<Memory>$(Memory.PadRight($([MSBuild]::Add($(Pointer[0]), 1)), %00))</Memory>
これはC#風のコードで表すと下記のようなものになります。
memory = memory.PadRight(pointer + 1, '\0');
Memoryへのアクセスの際にMemoryの長さが足りなくてエラーとならないように、MemoryをPointerの位置まで拡張しています。
なおここで、
$([MSBuild]::Add($(Pointer), 1))
ではなく、
$([MSBuild]::Add($(Pointer[0]), 1))
としていることに着目してください。後者はstringクラスのインデクサを使ってプロパティから文字を単体で取り出しています。
型の問題なのか、この2つは計算結果が異なります。
たとえばPointerの値が9
(ASCIIコードで57)の場合、前者の計算結果は10
になり、後者の計算結果は58
となります。
MSBuildはプロパティ関数実行時の型の扱いに謎なところが多く、今回は手探りでこういうことをいくらかやっています[^1]。ドキュメント外の挙動なので、バージョンが変わると動かないかもしれません。
(MSBuildはソース公開されてるはずなのでソース読んでおいた確実ですね(今回は時間が足りず読んでないのですが…))
命令文字の解釈
<Instruction>$(Input.Substring($(ProgramCounter[0]), 1))</Instruction>
<PointerIncrement Condition="'$(Instruction)' == '>'">1</PointerIncrement>
<PointerIncrement Condition="'$(Instruction)' == '<'">-1</PointerIncrement>
<MemoryIncrement Condition="'$(Instruction)' == '+'">1</MemoryIncrement>
<MemoryIncrement Condition="'$(Instruction)' == '-'">-1</MemoryIncrement>
<LoopStart Condition="'$(Instruction)' == '['">1</LoopStart>
<LoopEnd Condition="'$(Instruction)' == ']'">1</LoopEnd>
<OutputString Condition="'$(Instruction)' == '.'">$(Memory.get_Chars($(Pointer[0])))</OutputString>
これはC#風のコードで表すと下記のようなものになります。
instruction = input.Substring(programCounter, 1);
if (instruction == ">") pointerIncrement = 1;
if (instruction == "<") pointerIncrement = -1;
if (instruction == "+") memoryIncrement = 1;
if (instruction == "-") memoryIncrement = -1;
if (instruction == "[") loopStart = true;
if (instruction == "]") loopEnd = true;
if (instruction == ".") outputString = memory[pointer];
後の処理に備えて、命令文字を解釈して各プロパティにセットしています。
ポインタの処理
<PropertyGroup Condition="'$(PointerIncrement)' != '0' And '$(LoopSkipDepth)' == '%00'">
<Pointer>$([System.Convert]::ToChar($([MSBuild]::Add($(Pointer[0]), $(PointerIncrement)))))</Pointer>
</PropertyGroup>
これはC#風のコードで表すと下記のようなものになります。
if (pointerIncrement != 0 && loopSkipDepth == 0)
{
pointer = pointer + pointerIncrement;
}
そのまんま、ポインタをずらしているだけです。LoopSkipDepthについては先の説明を参照してください。
Pointerの代入時に行っているToCharについて少し説明します。わかりづらい括弧のネストにインデントをつけると次のようになります。
$([System.Convert]::ToChar(
$([MSBuild]::Add(
$(Pointer[0]),
$(PointerIncrement)
))
))
Pointerの値を取り出すときにインデクサを使っている理由については先に書いた通りです。
ここでAddによって返された計算結果をToCharに渡しています。
Addは数値を返しますが、その値をそのままプロパティに代入するとそれはその数値を表す文字列に変換されてしまいます。
たとえば計算結果が9だった場合、代入される値は9という文字列になってしまいます。
しかし先に書いた通り、今回は値はすべてそれを文字コードとする文字列にシリアライズしているので、その変換のためにここではToCharメソッドを通しています。
プロパティに設定しようとしている値が数値の場合はそれが数値文字列に変換されるのですが、charの場合にはそのcharがそのまま文字としてプロパティに設定されるようなので、それを利用しています。
メモリの処理
<PropertyGroup Condition="'$(MemoryIncrement)' != '0' And '$(LoopSkipDepth)' == '%00'">
<MemoryBeforePointer>$(Memory.Substring(0, $(Pointer[0])))</MemoryBeforePointer>
<MemoryOnPointer>$([System.Convert]::ToChar($([MSBuild]::Add($(Memory.get_Chars($(Pointer[0]))), $(MemoryIncrement)))))</MemoryOnPointer>
<MemoryAfterPointer>$(Memory.Substring($([MSBuild]::Add($(Pointer[0]), 1))))</MemoryAfterPointer>
<Memory>$(MemoryBeforePointer)$(MemoryOnPointer)$(MemoryAfterPointer)</Memory>
</PropertyGroup>
これはC#風のコードで表すと下記のようなものになります。
if (memoryIncrement != 0 && loopSkipDepth == 0)
{
var memoryBeforePointer = memory.Substring(0, pointer);
var memoryOnPointer = memory[pointer] + memoryIncrement;
var memoryAfterPointer = memory.Substring(pointer + 1);
memory = Concat(memoryBeforePointer, memoryOnPointer, memoryAfterPointer);
}
Memoryをポインタの指し示す前の部分、ポインタの指し示す部分、ポインタの指し示す後の部分にわけて取り出した後、値を更新して再構成しています。
ループの開始処理
<PropertyGroup Condition="'$(LoopStart)' != '0'">
<LoopStack>$(LoopStack)$(ProgramCounter)</LoopStack>
<LoopSkipDepth Condition="'$(LoopSkipDepth)' == '%00' And '$(Memory.get_Chars($(Pointer[0])))' == '%00'">$([System.Convert]::ToChar($(LoopStack.Length)))</LoopSkipDepth>
</PropertyGroup>
これはC#風のコードで表すと下記のようなものになります。
if (loopStart)
{
loopStack.Append(programCounter);
if (loopSkipDepth == 0 && memory[pointer] == 0) loopSkipDepth = loopStack.Length;
}
先に説明したループの処理の通りです。
ループの終了処理
<PropertyGroup Condition="'$(LoopEnd)' != '0' And '$(Memory.get_Chars($(Pointer[0])))' != '%00'">
<LoopStackLastIndex>$([MSBuild]::Subtract($(LoopStack.Length), 1))</LoopStackLastIndex>
<ProgramCounter>$(LoopStack[$(LoopStackLastIndex)])</ProgramCounter>
</PropertyGroup>
<PropertyGroup Condition="'$(LoopEnd)' != '0' And '$(Memory.get_Chars($(Pointer[0])))' == '%00'">
<LoopSkipDepth Condition="'$([MSBuild]::Subtract($(LoopSkipDepth[0]), $(LoopStack.Length)))' == '0'">%00</LoopSkipDepth>
<LoopStack>$(LoopStack.Substring(0, $([MSBuild]::Subtract($(LoopStack.Length), 1))))</LoopStack>
</PropertyGroup>
これはC#風のコードで表すと下記のようなものになります。
if (loopEnd && memory[pointer] != 0)
{
var loopStackLastIndex = loopStack.Length - 1;
programCounter = loopStack[loopStackLastIndex];
}
if (loopEnd && memory[pointer] == 0)
{
if (loopSkipDepth == loopStack.Length) loopSkipDepth = 0;
loopStack = loopStack.Substring(0, loopStack.Length - 1);
}
文字の出力
<Message Condition="'$(OutputString)' != '' And '$(LoopSkipDepth)' == '%00'" Text="$(OutputString)" Importance="high" />
これはC#風のコードで表すと下記のようなものになります。
if (outputString != "" && loopSkipDepth == 0)
{
Console.Write(outputString);
}
Hello World!
> MSBuild.exe BF.proj -v:m -p:Input="+++++++++[>++++++++>+++++++++++>+++>+<<<<-]>.>++.+++++++..+++.>+++++.<<+++++++++++++++.>.+++.------.--------.>+.>+."
.NET Framework 向け Microsoft (R) Build Engine バージョン 15.9.21+g9802d43bc3
Copyright (C) Microsoft Corporation.All rights reserved.
H
e
l
l
o
W
o
r
l
d
!
Hello World!のプログラムはこちらの記事のものを使わせていただきました。
おわり
おわりです。
実装や解説の間違い等あればご指摘ください。
[^1]: いくらか試した感じの推測だと、プロパティの値を取り出す際はあくまでstring型として取り出され、それをメソッドに渡すときにメソッドの引数が数値型を要求していれば数値文字列としてパースされ、またメソッドから返された値は再びプロパティに代入されるまでの間は型情報が保持されるという感じかと思います。たぶん。きっと。