12. 浮動小数点演算 その2

前回は一般的な浮動小数点の取り扱いに関する解説でしたが,今回は実際に 浮動小数点演算を利用する実用的なプログラムを作成します.

ちょっと安易な発想ですが「電卓」にします.

    12345.678 * 12345 / 3.14 =

と入力した場合に

    4.8537426e7

というように結果が返ることを考えます. この場合1行入力された式を 解析 (parse) する必要があり,コンパイラやインタプリタのような 言語処理系を作成する基礎となります.演算子に優先順位をつけ,カッコ を含む式を扱えるようにすると,サンプルとしては長すぎるプログラムに なってしまうので:

  1. 演算子に優先順位はない.(乗算や除算は, 加算や減算に優先しない)
  2. カッコを含む式は不可.
  3. 入力する式の行編集を可能にする.

という仕様で電卓を作ってみます.それでも昔の Tiny BASIC のようなインタプリタを作成する上で重要な多くの部分を含みます.

さて,行編集のために 10. コンソール入力の編集 で作成した READ_LINE サブルーチンや termios 制御のサブルーチンを利用します. また,前回の文字列と浮動小数点数の相互変換のサブルーチン, 5. 標準入出力用のサブルーチンの作成 も利用できます. この辺までくるとアセンブラのプログラムもずいぶんと楽になってきます.

プログラムの流れは次の様になります.

  1. 1行入力
  2. 入力行を最初から順に解析して実行,1.に戻る
    • 演算子なら登録
    • 数値なら演算子が記録されていれば計算,演算子がなければ数値を登録
    • コマンドなら実行
;---------------------------------------------------------------------
;   Floating Point
;   2000/11/04 Jun Mizutani
;   calc.asm
;---------------------------------------------------------------------

%include "stdio.inc"
%include "float.inc"
%include "readline.inc"

%assign         MAXLINE 256
%assign         MAXWORD 256

%assign         OP_NULL 0
%assign         OP_ADD  1
%assign         OP_SUB  2
%assign         OP_MUL  3
%assign         OP_DIV  4
%assign         OP_ATAN 5

;==============================================================
section .text
global _start

_start:
                finit                           ; FPU を初期化
                call    GET_TERMIOS             ; termios の保存
                call    SET_TERMIOS             ; 端末のローカルエコーをOFF
                fldz                            ; 0 をFPUにロード
                mov    byte[level], 0           ; レジスタスタックレベル
ReadLine:       ; 1行入力
                mov     eax, prompt             ; プロンプト表示
                call    OutAsciiZ
                mov     eax, MAXLINE            ; 1 行入力
                mov     ebx, input
                call    READ_LINE
                mov     ecx, MAXLINE            ; 行前処理
                mov     esi, input
                call    LineCleaner
                mov     eax, esi                ; CHECK!!
                mov     byte[operation], OP_NULL
                xor     ebx, ebx                ; 行内オフセット = 0
Parse:          ; 行解析
                mov     eax, 0x20               ; 空白スキップ
                mov     ecx, MAXLINE            ; reserved
                mov     esi, input
                call    SkipChar
                mov     eax, 0x20               ; 一語取得
                mov     edi, wordbuf
                call    Word1
                jb      ReadLine
                cmp     edx, 0
                jz      ReadLine                ; empty

                mov     al, [edi]
                cmp     al, "0"                 ; 数字か?
                jb      .command
                cmp     al, "9"
                ja      .command
                jmp     .num                    ; 0 - 9 なら数値入力

.command        ; 1文字コマンド
                cmp     edx, 1
                jne     near .num               ; 1文字以上は数値
                cmp     al, 'Q'                 ; Q, q ならば終了
                jne     .com1
                call    RESTORE_TERMIOS         ; termios の復帰
                call    Exit
.com1
                cmp     al, '.'                 ; 数値の可能性有り
                jne     .com2
                dec     ebx                     ; 1文字戻す(ungetc)
                jmp     .num
.com2
                cmp     al, '+'                 ; 加算
                jne     .com3
                mov     byte[operation], OP_ADD
                jmp     Parse
.com3
                cmp     al, '-'                 ; 減算
                jne     .com4
                mov     byte[operation], OP_SUB
                jmp     Parse
.com4
                cmp     al, '*'                 ; 乗算
                jne     .com5
                mov     byte[operation], OP_MUL
                jmp     Parse
.com5
                cmp     al, '/'                 ; 除算
                jne     .com6
                mov     byte[operation], OP_DIV
                jmp     Parse
.com6
                cmp     al, '='                 ; 数値の表示
                jne     .com7
                fld     st0                     ; 複製 (DUP)
                call    print_float             ; 表示
                jmp     Parse

.com7
                cmp     al, 'D'                 ; スタック位置表示
                jne     .com8
                mov     eax, '<'
                call    OutChar
                fstsw   ax
                shr     eax, 11
                and     eax, 0x07
                call    PrintLeft               ; 表示
                mov     eax, '>'
                call    OutChar
                call    NewLine
                jmp     Parse
.com8
                cmp     al, 'R'                 ; 平方根
                jne     .com9
                fsqrt
                jmp     Parse
.com9
                cmp     al, 'S'                 ; サイン
                jne     .com10
                fsin
                jmp     Parse
.com10
                cmp     al, 'C'                 ; コサイン
                jne     .com11
                fcos
                jmp     Parse
.com11
                cmp     al, 'T'                 ; タンジェント
                jne     .com12
                fptan
                jmp     Parse
.com12
                cmp     al, '$'                 ; アークタンジェント
                jne     .com13
                mov     byte[operation], OP_ATAN
                jmp     Parse
.com13
                cmp     al, 'P'                 ; π
                jne     .com14
                cmp     byte[operation], OP_NULL
                jne     .com13_1
                ffree   st0                     ;
                fincstp
.com13_1:       fldpi
                jmp     .op_add
.com14
                cmp     al, 'H'                 ; ヘルプ表示
                jne     .com15
                mov     eax, help1
                call    OutAsciiZ
                mov     eax, help2
                call    OutAsciiZ
                mov     eax, help3
                call    OutAsciiZ
                mov     eax, help4
                call    OutAsciiZ
                mov     eax, help5
                call    OutAsciiZ
                jmp     Parse
.com15
                jmp     Parse

.num            ; 数値
    .op_num:    mov     eax, wordbuf
                call    Ascii2Float      ; 浮動小数点数の文字列をFPUにセット
                jb      near .num_err           ; 数値変換エラー
                cmp     byte[operation], OP_NULL
                jne     .op_add
                fxch                            ; スタックを調節
                ffree   st0
                fincstp                         ; pop

    .op_add:    cmp     byte[operation], OP_ADD
                jne     .op_sub
                faddp   st1,st0
                mov     byte[operation], OP_NULL
                jmp     Parse
    .op_sub:    cmp     byte[operation], OP_SUB
                jne     .op_mul
                fsubp   st1,st0
                mov     byte[operation], OP_NULL
                jmp     Parse
    .op_mul:    cmp     byte[operation], OP_MUL
                jne     .op_div
                fmulp   st1,st0
                mov     byte[operation], OP_NULL
                jmp     Parse
    .op_div:    cmp     byte[operation], OP_DIV
                jne     .op_atan
                fdivp   st1,st0
                mov     byte[operation], OP_NULL
                jmp     Parse
    .op_atan:   cmp     byte[operation], OP_ATAN
                jne     .op_end
                fpatan                          ; atan(st1/st0)
                mov     byte[operation], OP_NULL
                jmp     Parse
    .op_end:
                jmp     Parse
.num_err
                mov     eax, numerror           ; 変換エラー表示
                call    OutAsciiZ
                mov     eax, wordbuf
                call    OutAsciiZ
                call    NewLine
                jmp     Parse

;------------------------------------
; FPU のスタックトップの浮動小数点レジスタを文字列に変換して表示
; スタックトップは POP (廃棄) される.必要なら先に DUP する.
print_float
                mov     edi, fstring
                call    Float2Ascii
                mov     eax, ' '
                call    OutChar
                mov     eax, fstring
                call    OutAsciiZ
                call    NewLine
                ret

;--------------------------------------------------------------
; esi で指定され, 長さ ecx の文字列バッファ領域の文字を変換
; 英小文字は英大文字,TAB は SPACE, LF は 0 に変換される.
; ecx : buffer length        (reserved)
; esi : buffer start address (reserved)
;--------------------------------------------------------------
LineCleaner:
                jcxz    .lc_end
                push    esi
                push    ecx
        .next
                mov     al, [esi]
                or      al, al
                jz      .lc_end
                cmp     al, 0x09                ; TAB
                jne     .lc_lf
                mov     byte [esi], 0x20        ; SPACE
                jmp     short .skip
        .lc_lf
                cmp     al, 0x0A                ; Line feed
                jne     .lc_upper
                mov     byte [esi], 0           ; 0
                jmp     short .skip
        .lc_upper
                cmp     al, 0x60
                jb      .skip
                cmp     al, 0x7B
                jae     .skip
                sub     al, 0x20
                mov     byte [esi], al          ; uppercase
        .skip
                inc     esi
                loop    .next
        .lc_end
                pop     ecx
                pop     esi
                ret

;--------------------------------------------------------------
; esi で指定される文字列バッファ領域中のオフセット ebx から
; al で指定される文字を読み飛ばして,al 以外の文字まで
; ebx を進める. バッファ領域を超えると何もしない.
; al  : skip character       (reserved)
; ebx : start offset within buffer, return new offset
; ecx : buffer length        (reserved)
; esi : buffer start address (reserved)
;--------------------------------------------------------------
SkipChar:
        .skip
                cmp     ebx, ecx
                jns     .skipend
                cmp     al, [esi + ebx]
                jne     .skipend
                inc     ebx
                jmp     .skip
        .skipend
                ret

;--------------------------------------------------------------
; 区切り文字か, 行末か, 行バッファ末までの文字列を
; edi からの語句バッファにコピー.
; 語句バッファの文字列末に 0 を設定
; 行バッファをすべて使ったらキャリーセット
; コピーした文字列長(0を含まない) を edx に返す.
; eax : delimiter
; ebx : start offset within buffer, return new offset
; ecx : line buffer length (reserved)
; edx : return word length
; esi : line buffer start address (reserved)
; edi : word buffer start address (reserved)
; Set CF=1, if line buffer contains no data.
;--------------------------------------------------------------
Word1:
                xor     edx, edx
        .word
                cmp     ebx, ecx
                jns     .wend                   ; 範囲外 ?
                mov     ah, [ebx + esi]
                or      ah, ah                  ; 0 ?
                je      .wend
                inc     ebx
                cmp     ah, al                  ; 区切り文字
                jne     .copy
                or      edx, edx
                jne     .wend
                jmp     short .word
        .copy
                mov     [edx + edi], ah
                inc     edx
                jmp     short .word
        .wend
                mov     byte [edx + edi], 0
                or      edx, edx
                je      .empty
                clc
                ret
        .empty
                stc
                ret

;--------------------------------------------------------------
; 現在の termios を保存
GET_TERMIOS:
                pusha
                mov     ebx, old_termios
                call    tcgetattr
                mov     ecx,  new_termios - old_termios
                mov     esi,  old_termios
                mov     edi,  new_termios
                rep
                movsb
                popa
                ret

;--------------------------------------------------------------
; 新しい termios を設定
; Rawモード, ECHO 無し, ECHONL 無し
; VTIME=0, VMIN=1 : 1バイト読み取られるまで待機
SET_TERMIOS:
                pusha
                mov     eax, [new_termios.c_lflag]
                and     eax, ~ICANON & ~ECHO & ~ECHONL
                or      eax, ISIG
                mov     [new_termios.c_lflag], eax
                mov     eax, 1
                mov     [new_termios.c_cc + VMIN], al
                mov     eax, 0
                mov     [new_termios.c_cc + VTIME], al
                mov     ebx, new_termios
                call    tcsetattr
                popa
                ret

;--------------------------------------------------------------
; 保存されていた termios を復帰
RESTORE_TERMIOS:
                pusha
                mov     ebx, old_termios
                call    tcsetattr
                popa
                ret

;--------------------------------------------------------------
; 標準入力の termios の取得と設定
; tcgetattr(&termios)
; tcsetattr(&termios)
; eax : destroyed
; ebx : termios buffer adress
; ecx, edx : destroyed
tcgetattr:
                mov     eax, TCGETS
                jmp     short IOCTL

tcsetattr:
                mov     eax, TCSETS

;--------------------------------------------------------------
; 標準入力の ioctl の実行
; sys_ioctl(unsigned int fd, unsigned int cmd,
;           unsigned long arg)
; eax : cmd
; ebx : buffer adress
IOCTL:
                mov     ecx, eax            ; set cmd
                mov     edx, ebx            ; set arg
                mov     eax, 54             ; sys_ioctl
                mov     ebx, 0              ; to stdin
                int     0x80                ; call kernel
                ret

;==============================================================
section .bss

fstring         resb 32
input           resb MAXLINE
wordbuf         resb MAXWORD
operation       resb 1

old_termios     istruc termios
                  .c_iflag    UINT   1       ; input mode flags
                  .c_oflag    UINT   1       ; output mode flags
                  .c_cflag    UINT   1       ; control mode flags
                  .c_lflag    UINT   1       ; local mode flags
                  .c_line     UCHAR  1       ; line discipline
                  .c_cc       UCHAR  NCCS    ; control characters
                iend

new_termios     istruc termios
                  .c_iflag    UINT   1       ; input mode flags
                  .c_oflag    UINT   1       ; output mode flags
                  .c_cflag    UINT   1       ; control mode flags
                  .c_lflag    UINT   1       ; local mode flags
                  .c_line     UCHAR  1       ; line discipline
                  .c_cc       UCHAR  NCCS    ; control characters
                iend
new_termios_end

;==============================================================
section .data

level           db   0
prompt          db   'calc>',0
help1           db   ' LineEdit  ^H:BS  ^D:Delete  ^B:Back  ^F:Forward', 0x0A, 0
help2           db   ' Function  R:SquareRoot  S:Sine  C:Cosine', 0x0A, 0
help3           db   '           T:Tangent  $:atan(a/b)= a $ b  P:Pi', 0x0A, 0
help4           db   ' Display   =:Result   D:StackLevel  H:Help', 0x0A, 0
help5           db   ' Quit      Q', 0x0A, 0
numerror        db   ' Error>',0

入力行はタブがあればスペースに変換され,英小文字は大文字に変換された後に スペースを区切りとして単語に分離して解析,実行されます.演算子,コマンドは 1文字になっているため,2 文字以上の単語は数値と解釈され数値でなければ エラーとなり無視されます.

実行結果

jm:~/ex_asm$ asm calc
jm:~/ex_asm$ ./calc
calc>h
 LineEdit  ^H:BS  ^D:Delete  ^B:Back  ^F:Forward
 Function  R:SquareRoot  S:Sine  C:Cosine
           T:Tangent  $:atan(a/b)= a $ b  P:Pi
 Display   =:Result   D:StackLevel  H:Help
 Quit      Q
calc>1.0 + 2.0 * 3 =
 9.00000000000000000
calc>/10.0
 Error>/10.0
calc>/ 10.0 =
 9.00000000000000000e-1
calc>p / 6 s =
 5.00000000000000000e-1
calc>p / 6 c =
 8.66025403784438647e-1
calc>3 r / 2 =
 8.66025403784438647e-1
calc>d
<7>
calc>q

使用方法は H コマンドで表示されます.終了は Q です. コマンドは小文字でも 内部で大文字に変換されるため,どちらでもかまいません.D コマンドは FPU の スタックの位置を表示します.常に <7> が表示されますが,calc を拡張する 場合に FPU のデータレジスタスタックの状態を確認するために使ってください. 0 除算などの例外のチェックはしていません.0 除算をしてもエラーとはならず,不正な数値が 設定されます.

対数や指数の計算は実装していません.拡張してみてください.