ほのぼのとまったりアプリ開発日誌

ほのぼのがまったりとデスクトップアプリを作る開発日誌

language-hsp3 for VSCode を公開しました。

梅雨の季節になりました。木くらげ酢の物を食べています。

令和に入る前頃にVSCodeでemojiにヒットする文字プロパティを見つけることができたので、atom向けに公開している language-hsp3 の完全移植ができました。

marketplace.visualstudio.com

移植元と相違点があります。主に機能追加と不具合修正です。

構文ハイライト

  • 数字
    • 負号を演算子としてハイライトしていたのを数字の一部に含むよう変更しました。
    • 浮動小数点記法が正しくハイライトされないのを修正しました。
  • ラベル
    • 命令/関数の第一引数に書かれたラベルをハイライトできるようになりました。
  • 命令
    • 命令をハイライトできるようになりました。
  • 変数
    • 変数初期化文を変数としてハイライトできるようになりました。

コマンド

wineを簡単に使用する機能を追加しました。 wineでコンパイラを実行、ファイルパスをwinepathでWindows形式に変換するので、hspcmp.exeも使用できるようになります。

ショートカットキー

F5は、デバッガー機能で埋まっていたので、Ctrl-K F5に設定しました。 Ctrl-Kを押した後にf5を押すことで実行されます。 Ctrl-F9は移植元と同じです。

VSCode のエディタ設定

今現在、HSP3のサンプルソースファイルで使われるコードページの多くは Shift_JIS です。

VSCodeをインストールした直後だと、コードページの既定値はUTF-8になっています。

そのため、HSPのソースファイルを開くたびに、Shift_JIS で開き直す場面に出会うと思います。

VSCodeには、言語ごとにエディタの設定を変更する機能が備わっています。

vscode - Visual Studio Codeで言語ごとにインデントの設定をしたい - スタック・オーバーフロー

qiita.com

VSCodeの設定画面で ”ワークスペース” を選択した後に "{}" マークをクリックして、開かれた settings.json ファイルに以下のJSONコードを設定してみてください。

{
  "[hsp3]" : { "files.encoding": "shiftjis" },
  "files.eol": "\r\n"
}

これで、*.hsp, *.as ファイルは Shift_JIS が既定のコードページに設定されます。

HSPスクリプトエディタの仕様に合わせるなら下記のように設定します。

{
  "[hsp3]" : {
    "files.encoding": "shiftjis",
    "editor.detectIndentation": false,
    "editor.insertSpaces": false,
    "editor.tabSize": 4
  },
  "files.eol": "\r\n"
}

蒸し暑いと酢の物が食べたくなります。鶏のから揚げには、ゆず酢やレモン果汁をかけています。これでしっかり体力を補います。

熱中症、脱水症に気を付けて、無理せず頑張りましょう。

linter-hsp3 v0.2.0 を公開しました。

まだまだ寒い気候が続いています。今年も旬の菜の花を頂きました。美味しかったです。

atomにHSP3のソースファイルをコンパイルしてエラーをlinterに反映させるパッケージ、linter-hsp3 をアップデートしました。

主な変更点は非同期化で起こったhsptmpファイルの不整合が生じる問題の修正と、全体的にPromise化したことです。

v0.1.Xでは、hsptmpファイルに対して重複して解析が走っていました。hsptmpファイルの解析中に新しいバージョンがhsptmpファイルに書き込まれることで、古い方の処理で例外が生じていました。

v0.2.0には、async-lock モジュールを使用して、排他制御を行いました。解析処理が一つずつ行われるので、linter-hsp3の処理速度は落ちますが、安定したと思います。

さらに、エラーを起こしたソースファイルのコードページ推測に encoding.js(モジュール名はencoding-japanese)を使用しました。文字化けの抑制を図ります。

上記の変更を加える為にコールバック地獄と化した処理部分を書き直しました。

処理の大部分はPromiseになりました。すごく書きやすかったです。

インフルエンザが猛威を振るっています。外出の際はうっかり目や鼻をこすらないよう気を付けて。外で携帯電話を触ったら、忘れず帰宅時に消毒しましょう。

摩訶不思議な #undef

追記 2020/10/20 一部コンパイラの最適化処理により正しくない説明を記載していました。ご迷惑をお掛けしましたことをお詫びするとともに、記事内に訂正させていただきます。


linter-hsp3 をデバッグするには、わざとエラーになるソースコードが必要です。

なので、様々なシチュエーションを想像して、コードを書きました。

その時に見つけた不思議なエラーたちをここで紹介したいと思います。

また、紹介するコードは HSP スクリプトエディタで実行できるので、試してみて下さい。

#undef の普通の使い方

エラーを紹介する前に、正しい #undef の使い方を見てみましょう。

#define で登録した名称を取り除く使い方

#undef の機能は、指定したマクロ名(名称とも)を取り除くことです。

取り除かれた名称は、コメントアウトのような状態になります。

#define M
#undef M
#ifdef M
mes "use M"
#else
mes "no use M"
#endif

F5で実行した結果は…

no use M

#ifdef は、マクロ名称が登録されていれば、#else か #endif までをコンパイルします。そして、#else から #endif までのコードを取り除きます。

これで、#define で登録した M という名称が #undef で取り除かれたことが分かりました。

ここで疑問が浮かびました… #undef を使った位置がずれていたらどうなるのでしょうか?

とても興味深いことです。やってみましょう!

#define M
#ifdef M
mes "use M"
#else
mes "no use M"
#endif

#undef M

; try again
#ifdef M
mes "use M"
#else
mes "no use M"
#endif

実行した結果は…

use M
no use M

一行目は #undef で取り除く前のコードです。そして、二行目は #undef された後のコードです。つまり、#define と #undef は、HSP3のプログラムのように、コードの上から登録と取り除きが行われています!

この特徴を使えば、重複して同じソースファイルをincludeするのを防ぐことができます。

common\user32.as の最初と最後を見てみると…

;(user32.as)
#ifdef __hsp30__
#ifndef __USER32__
#define global __USER32__
#uselib "USER32.DLL"
   #func global ActivateKeyboardLayout "ActivateKeyboardLayout" sptr,sptr
   #func global AdjustWindowRect "AdjustWindowRect" sptr,sptr,sptr
    ~ ~ ~ 中略 ~ ~ ~
   #func global wvsprintfA "wvsprintfA" sptr,sptr,sptr
   #func global wvsprintfW "wvsprintfW" wptr,wptr,wptr
#endif
#endif

#ifndef でマクロが登録されていなければ、コンパイルするように囲まれています。試しに二回includeしてみましょう。

#include "user32.as"
#include "user32.as"
if varptr(MessageBox) != 0 :mes "ok"    ; 最適化対策

実行結果は、ちゃんとエラーにならずに ok と表示されました!

これ以外にも、様々な使い方ができます。

単体で実行するとテストを走らせる。前提となるソースファイルをincludeするなど、他には……

……これ以上広げると、#undefじゃない話になりそうなので、元に戻します。

標準キーワードを置き換える使い方

mes などの最初から使えるキーワードを標準キーワードと呼びます。

#undef は、標準キーワードを取り除くことができます。

取り除いた後に、#deffunc などで名称登録すれば、標準キーワードを置き換えられます。

やってみましょう!mes命令を猫が喋るように置き換えてみます。

goto *exit
#undef mes
#deffunc mes str p1, int p2
  mes@hsp "猫「"+p1+"」", p2
  return
*exit
mes "よろしくお願いします。"

実行結果は…

猫「よろしくお願いします。」

猫が喋ってます。うまくいきました!

主な使い道は、使ってはいけない標準キーワードを使っている外部ソースファイルを見つけたり、機能を拡張させることでしょうか。

標準キーワードを変更する場合、影響範囲が大きくなるので、注意が必要です。

#undef のあやしい使い方

ドキュメントに書かれていた使い方は、先の二つだけです。つまり、それ以外は未知の動作……洞窟探検の気分で挑みましょう。

#module は置き換えられない

nekoモジュールに age というテーブル変数を追加しようと、#undef を使って置き換えるコードです。

#module neko name
#modinit str p1
  name = p1
  return
#global

#undef neko
#module neko name, age
#modinit str p1, int p2
  name = p1
  age = p2
  return
#global

dimtype this, vartype("struct")
newmod this, neko, "tama"

しかし、コンパイルエラーになります。エラーメッセージは…

#Error:定義済みの識別子は使用できません [name@neko] in line 8 [blog\02.hsp]
#重大なエラーが検出されています

どうやら、モジュール名を取り除くだけでは付随したテーブル変数まで除いてくれないようです。

では、その変数も #undef で取り除いてみましょう。以下のコードを挿入します…

#undef neko
#undef name@neko    ; 挿入
#module neko name, age

編集した結果は、エラーです!

#識別子「neko」の定義位置: line 3 in [blog\02.hsp]
blog\02.hsp(11) : error 26 : パラメーター引数名は使用されています (11行目)
--> #struct neko var name@neko,var age@neko

テーブル変数は、名称だけでなく、パラメーター引数名として登録されていました。これで #undef では取り除けないことが分かりました!

追記 2020/10/20

#deffunc は置き換えられない

#deffunc で登録した名称を置き換える

モジュールが置き換えられないなら、#deffunc はどうでしょうか?

やってみましょう!

まず最初にneko命令を登録します。それを#undefで取り除いてから、mes命令の第二引数に対応した新しいneko命令を登録します。

#module
#deffunc neko str p1
  mes "猫「"+p1+"」"
  return
#global

#undef neko
#module
#deffunc neko str p1, int p2
  mes "猫「"+p1+"」", p2
  return
#global

neko "よろしくお願いします。", 1
neko "猫です。", 1

実行結果は…

猫「よろしくお願いします。」猫「猫です。」

うまくいきました!

#deffunc で登録した名称は、#undef で取り除き、再度登録することができるのが分かりました。

#module
#deffunc neko str p1
  mes "猫「"+p1+"」"
  return
#global
neko "nya-go"  // 追加
#undef neko
#module
#deffunc neko str p1, int p2
  mes "猫「"+p1+"」", p2
  return
#global

neko "よろしくお願いします。", 1
neko "猫です。", 1

上記コードの中央#undef nekoの上の行にneko "nya-go"を追加してコンパイルすると、

blog\07.hsp(9) : error 25 : 命令として定義できない名前です (9行目)
--> #deffunc neko str p1@m1,int p2@m1

のエラーメッセージが出力されました。

#deffunc で登録した名称は、#undef で取り除き、再度登録することが出来ない事がわかりました。

#modfunc は置き換えられない

では #modfunc はどうでしょうか?モジュールが置き換えられないことが分かっていますが…

#module neko name
#modinit str p1
  name = p1
  return
#modfunc show
  mes name
  return
#global

#undef show
#module mod v1
#modfunc show
  mes "名前は"+v1+"です。"
  return
#global

  newmod this, neko, "maro"
  show this

最初に登録したshowメソッドを置き換えるコードです。結果はコンパイルエラーになりました。

blog\08.hsp(12) : error 25 : 命令として定義できない名前です (12行目)
--> #deffunc show modvar mod

しかし、真ん中でメソッドを呼ぶとコンパイルエラーは発生しませんでした。

#module neko name
#modinit str p1
  name = p1
  return
#modfunc show
  mes name
  return
#global

  newmod this, neko, "maro"
  show this

#undef show
#module mod v1
#modfunc show
  mes "名前は"+v1+"です。"
  return
#global

実行結果は、maro と表示されました。おそらく最適化によって、置き換え先のモジュール諸共は削除されてしまったのでしょう。

追記 2020/10/20 考察通りでした。ソースコード最後にshow thisを追加すると

コンパイルエラー `error 25 : 命令として定義できない名前です (15行目)
--> #deffunc show modvar mod

が出力されます。

#const/#enum で登録した名称を置き換える

#defineが置き換えられたのなら、定数を登録する #const と #enum でも同様に置き換えらそうです。

やってみましょう!

#enum ringo = 88
#const mikan 398

#undef ringo
#enum ringo = 100
#undef mikan
#const mikan 789

mes ringo
mes mikan

実行結果は…

100
789

期待通りです!#define と同じように使えることが分かりました。

#func で登録した名称は置き換えられない

include先で外部DLLが登録されているシチュエーションは多いと思います。1

#uselib "user32.dll"
#func global MessageBox "MessageBoxA" sptr,sptr,sptr,sptr

#undef MessageBox
#func global MessageBox "MessageBoxA" sptr,sptr,sptr,sptr

if varptr(MessageBox) != 0 :mes "ok"

色々と試しましたが、#undef は #func で登録した名称を取り除くことはできませでした。コンパイルエラーのメッセージは以下の通りです。

#識別子「messagebox」の定義位置: line 2 in [blog\10.hsp]
blog\10.hsp(5) : error 30 : 拡張命令の名前はすでに使用されています (5行目)
--> #func messagebox "MessageBoxA" sptr,sptr,sptr,sptr

この場合の迂回策は、名称を名前空間付きで登録することです。

#uselib "user32.dll"
#func global MessageBox "MessageBoxA" sptr,sptr,sptr,sptr
#func MessageBox@my "MessageBoxA" sptr,sptr,sptr,sptr

if varptr(MessageBox) != 0 :mes "ok"
if varptr(MessageBox@my) != 0 :mes "ok"

コンパイルが通って、ok が二回表示されました。名前空間無しのグローバルな名称と、名前空間付きの名称は異なっていることが分かります。

名前空間が変わるモジュールを使えば、名前空間を書かなくても自動で名称に追加されるので、大量の#funcにも対応できます。さらに、別のモジュールで名前が登録されていても、名前空間がちがうので、名前の衝突を回避できます!

#module A
  #uselib "user32.dll"
  #func MessageBox "MessageBoxA" sptr,sptr,sptr,sptr

  #deffunc dialog@A
  ; 同じ名前空間のMessageBoxが呼ばれる
  MessageBox 0,"A","",""
  return
#global

#module B
  #uselib "user32.dll"
  #func MessageBox "MessageBoxW" wptr,wptr,wptr,wptr

  #deffunc dialog@B
  MessageBox 0,"B","",""
  return
#global

if varptr(MessageBox@A) != 0 :mes "ok"
if varptr(MessageBox@B) != 0 :mes "ok"
dialog@A
dialog@B

#cmd で登録した名称は取り除けられる

HSP3になってからは、見る機会が減った気がする #cmd でも #undef で取り除けられるか試してみます。

#runtime "hsp3gp"
#regcmd 9
#cmd gpreset  $60
#cmd gpgetlog $f6

#undef gpreset
#cmd gpreset  $60
#undef gpgetlog
#cmd gpgetlog $f6
gpreset 0
gpgetlog log
logmes log

真っ暗な画面が出てきました、次に #undef の後の #cmd をコメントアウトして実行してみます。

#runtime "hsp3gp"
#regcmd 9
#cmd gpreset  $60

#undef gpreset
gpreset 0

結果は、文法エラーになりました。

blog\11.hsp(6) : error 2 : 文法が間違っています (6行目)
--> gpreset 0

#cmd で登録された gpreset が、#undef で取り除かれているのが分かります!

おまけ 最適化の謎

#module mod
       #deffunc func int p1
        mes "func"+p1
    return
#global

    func 1

#undef func
#undef mod
#module mod
   #deffunc func int p1
        mes "func2:"+p1
    return
#global

    func 2

このまま実行すると、以下のエラーメッセージが表示されます。

blog\05.hsp(12) : error 25 : 命令として定義できない名前です (12行目)
--> #deffunc func int p1@mod

しかし、最後のfunc 2コメントアウトすると、エラーは解決します。そして、画面には func2:1 と表示されています!置き換えられています!

興味深いことに、error 25 は配列変数に対するエラーIDですが、メッセージとは異なっています。それに、”命令として定義できない名前です”というエラーメッセージに関する情報をhdlから見つけることはできませんでした。

さらに、先のコードに名前空間で書くことで、エラー発生行数をおかしくさせることもできました。

#module mod
   #deffunc local func int p1
        mes "func"+p1
    return
#global

func@mod 1

#undef func
#undef mod
#module mod
   #deffunc local func int p1
        mes "func2:"+p1
    return
#global

func@mod 2

エラーメッセージは…

(1701232) : error 25 : 命令として定義できない名前です (1701232行目)

行数だけでなく、ファイルパスも消えてしまいました。

おまけのおまけ

#cmpopt ppout 1
#module mod
   #deffunc func
        mes "func"
    return
#global

func@mod 1

#undef func
#undef mod
#module mod
   #deffunc local func
        mes "func2"
    return
#global

func@mod 2

同じ名前空間名でユーザー定義命令を書いてみたかったのですが、コンパイルは通りませんでした。

tmp.hsp(16) : error 7 : ラベル名はすでに使われています (16行目)
--> *_mod_exit

プログラム実行時、モジュール内へ突入を防止するためのラベル定義が干渉しているようです。

おわりに

何に対して #undef できるか、これで大雑把に分ったと思います。

今まで書いた記事の中で、一番長いものになりました。

まだ記事にするネタのストックはありますが、別の機会で書こうと思っています。


  1. 代表格は、#include “user32.as"と#include "llmod3/llmod3.hsp"の組み合わせだと思います。