Kanade Electronicskanaelecom
📜

自作言語:tasm

自作マイコン用の自作言語、tasm (typed assembler) です。

  • 組み込みシステムを作るために実用上十分な機能を持つこと
  • コンパイラの実装が簡単になること
  • 文法がわかりやすいこと

を目標に設計しました。

サンプルコード

type Vec2 = {x: int, y: int};

static origin: Vec2;

fn add(a: Vec2, b: Vec2) -> Vec2 {
    var result: Vec2 = {x: a.x + b.x, y: a.y + b.y};
    return result;
}

fn main() -> int {
    var points: [2]Vec2 = [{x: 1, y: 2}, {x: 3, y: 4}];
    var offset: Vec2 = {x: 10, y: 20};
    var sum: Vec2 = add(points[0], offset);
    return sum.x + sum.y;
}

トップレベルの定義

プログラムは定義 (def) の集合です。

記法
type Vec3 = {x: int, y: int, z: int};
定数const cst = 123;
静的変数static x: int;
アセンブリasm reset { jump(main); }
関数fn add(a: int, b: int) -> int { return a+b; }
def =
 | type   = 'type' ident '=' type ';'
 | const  = 'const' ?('@' expr) ident '=' expr ';'
 | static = 'static' ?('@' expr) ident ':' type ';'
 | asm    = 'asm' ?('@' expr) ident '{' asm_stmt* '}'
 | func   = 'fn' ident '(' args ')' ?('->' type) block

@ によるアドレス固定

const / static / asm は ident の前に @ expr を書くと、メモリ空間の固定アドレスに配置できます。MMIO 領域や割り込みベクタの配置に使います。

static @ 0x1000 serial_tx: int;     // MMIO
asm    @ 0x0000 reset { ... }       // リセットベクタ (imem)
asm    @ 0x0004 irq   { ... }       // 割り込みベクタ (imem)

指定しない場合は、リンカが各セクション (const / static / code) 内に自動で割り付けます。

記法
整数型int
void 型void
ポインタ型*int
配列型[N]int
構造体型{m0: int, m1: int}
関数型(arg: int) -> int
type =
 | int    = 'int'
 | void   = 'void'
 | custom = ident
 | addr   = '*' type
 | arr    = '[' expr ']' type
 | struct = '{' (ident ':' type) % ',' '}'
 | func   = '(' (ident ':' type) % ',' ')' '->' type

ポインタ型 * 配列型 [] を作るには、ベース型に前置します。英語の語順と同じです。たとえば *[2]int は “pointer to array of int” です。

型のサイズ

tasm は Data = 16bit / Address = 16bit のシステム向けに設計されています。型のサイズは次の規則でコンパイル時に計算されます。

<int>  = 1
<void> = 0
<*T>   = 1
<[N]T> = N × <T>
<{f₁: T₁, f₂: T₂, ...}> = <T₁> + <T₂> + ...
<(args) -> Ret> = 0

sizeof<T> または sizeof(expr) で 16bit ワード単位のサイズを取得できます。

参照演算子 / アドレス演算子

@a (前置) で a をポインタとみなして指す値を得ます。

a: *int@a: int

a@ (後置) で a のアドレスを得ます。

a: inta@: *int

前置 @ は値方向 (*T → T)、後置 @ はアドレス方向 (T → *T) と覚えると分かりやすいです。* は乗算と型表記 (*T) 専用です。

キャスト演算子

expr as Type で式の型を変換します。キャスト元とキャスト先の型サイズが一致している必要があります。

var p: *int = ...;
var n: int  = p as int;

サイズ不一致なキャストはコンパイルエラーです。

配列型

添字演算子を適用すると、ベースの型になります。

hoge: [N]inthoge[0]: int

多次元配列はこのように表されます。

hoge: [N][M]int

C 言語と異なり、配列とポインタの暗黙のキャストは行いません。配列の先頭要素のアドレスは

hoge: [N]inthoge[0]@: *int

配列全体のアドレスはこちら。値は一致しますが型が違います。

hoge: [N]inthoge@: *[N]int

構造体型

メンバ演算子でメンバの型になります。

hoge: {a: int}hoge.a: int

関数型

関数呼び出し演算子で返り値の型になります。

hoge: (arg: Arg) -> Rethoge(arg): Ret

関数型の変数は定義できません。代わりに関数ポインタ型を使います。

var hoge_p: *(arg: Arg) -> Ret = hoge@;
(@hoge_p)(arg);

関数本体は複文 (block) です。

stmt =
 | block    = ?( "'" ident ':' ) '{' stmt* '}'         # 複文
 | var      = 'var' ident ':' type ?('=' expr) ';'     # ローカル変数定義
 | if       = 'if' '(' expr ')' stmt ?('else' stmt)
 | while    = 'while' '(' expr ')' stmt
 | break    = 'break' "'" ident ';'
 | continue = 'continue' "'" ident ';'
 | return   = 'return' ?( expr ) ';'
 | assign   = expr '=' expr ';'                        # 代入文
 | expr     = expr ';'                                 # 式文

代入文

代入文が変数の値を書き換える唯一の方法です。左辺は lvalue (識別子・デリファレンス・添字・メンバアクセス)、右辺は値として評価されます。

a: int = b: int は内部的に a@: *int <= b: int と等価です。

ラベル付きブロックと break / continue

任意のブロック {}'label: 形式のラベルを付けられます。break 'label; はブロックの直後 (閉じ } の次) へ、continue 'label; はブロックの先頭へジャンプします。ラベルは省略不可で、見えないラベルを指定するとコンパイルエラーになります。

'outer: {
  while (i < n) {
    'inner: {
      while (j < m) {
        if (x) break 'outer;     // 'outer の直後へ
        if (y) continue 'inner;  // 'inner の先頭へ
        j = j + 1;
      }
    }
    i = i + 1;
  }
}

continue 'label; はブロックの先頭に戻るだけで、ブロック自体は自動でループしません。ループしたい場合は中に while を置きます。

演算子の優先順位

優先順位の低い順:

expr  = or
or    = xor ('|' xor)*
xor   = and ('^' and)*
and   = eq  ('&' eq)*
eq    = relat ('==' relat | '!=' relat)*
relat = shift ('<' shift | '<=' shift | '>' shift | '>=' shift)?
shift = add   ('<<' add | '>>' add)?
add   = mul   ('+' mul | '-' mul)*
mul   = unary ('*' unary | '/' unary | '%' unary)?
unary = post  | '+' unary | '-' unary | '!' unary | '@' unary
post  = prim
      | call   = post '(' expr % ',' ')'
      | index  = post '[' expr ']'
      | member = post '.' ident
      | ref    = post '@'
      | cast   = post 'as' type

prim =
 | number      # 10進: 123, 1_000  16進: 0x1234
 | char        # 'A', '\n'
 | string      # "Hello, World!\n"
 | ident
 | '(' expr ')'
 | '{' (ident ':' expr) % ',' '}'  # 構造体リテラル
 | '[' expr % ',' ']'              # 配列リテラル
 | 'sizeof' '<' type '>'           # 型のサイズ
 | 'sizeof' '(' expr ')'           # 式の型のサイズ

エスケープシーケンスは \n \t \r \\ \' \" \0 をサポートします。

アセンブリブロック

asm ブロックは命令を直接書く領域で、プログラムメモリ (imem) に配置されます。各行は「ゼロ個以上のラベル + 1 命令 + セミコロン」の形をとります。

asm_stmt = (ident ':')* ident '(' expr % ',' ')' ';'
           ^^^^^^^^^^^^ labels  ^^^^^^^^^^^^^^^^^^^^^ instruction

例:

asm @ 0x0000 reset {
    loadi(fp, 0xffff);
    call(init);
  halt:
    jumpr(halt);
}

命令

ニーモニックは inst(arg1, arg2, ...); の形で書きます。引数は式として評価され、レジスタ名・即値・グローバルシンボル・ローカルラベルのいずれかに解決されます。

種類ニーモニック
算術add sub addi subi
論理and or xor andi ori xori not
比較eq neq lt lts eqi neqi lti ltsi
シフトsr srs srr sl slr
転送mov loadi load store
制御nop jump jumpr jumpif jumpifr call callr ret iret

load(rd, base, imm) / store(rs, base, imm) の 3 引数形式は「ベース + オフセット」、2 引数で書くとゼロレジスタ基準の絶対アクセスになります。

詳細な機械語との対応は 命令セットアーキテクチャ を参照。

ABI (Application Binary Interface)

tasm の fn が前提とする呼出規約は次の通りです。

  • sp は廃止。スタックは fp のみで管理。
  • 全レジスタが caller-save (t0-t9ra 含む)。callee-save は存在しない。
  • 引数・戻り値はすべてスタック経由。callee 視点では fp + 1 が戻り値スロット、fp + 1 + i 以降が引数。
  • ローカル変数は fp からの負オフセットに確保。

詳しくは 関数呼び出し規約 を参照。