calc of emacs (2)

Table of Contents

emacs limit

前回sinをとんでもなく精度よく求める暴挙をしちゃった。その時

if: Lisp nesting exceeds ‘max-lisp-eval-depth’: 1601

こんな悲鳴を聞いたぞ。デカとしては、調べてみるしかないだろう。

vbox$ cd /usr/local/share/emacs/29.1/src/
vbox$ grep max-lisp-eval-depth *
bytecode.c:               error ("Lisp nesting exceeds `max-lisp-eval-depth'");
data.c:      "Lisp nesting exceeds `max-lisp-eval-depth'");
eval.c:  DEFVAR_INT ("max-lisp-eval-depth", max_lisp_eval_depth,
print.c:`max-lisp-eval-depth' being exceeded or an error may occur:

bytecode.c ああ、今はbytecodeを使っていないから、別の場所か。でも、他 を当たっても、ここっぽいなあ。gdbで追跡しても、ここで止まるし。。。

if (++lisp_eval_depth > max_lisp_eval_depth)
  {
    if (max_lisp_eval_depth < 100)
      max_lisp_eval_depth = 100;
    if (lisp_eval_depth > max_lisp_eval_depth)
      error ("Lisp nesting exceeds `max-lisp-eval-depth'");
  }

現実を直視するんだ。bytecodeを実行してるって事は、ソースコードをコンパ イルしてから実行してんだな。pythonだと、hoge.pyを実行すると、コンパイ ルした結果のhoge.pycが出現する。折角コンパイルしたんだから、保存してお いて、以後はコンパイル結果の方を使いましょ。そうすれば、コンパイル時間 も節約できる。

emacsはそこまでやってなくて、ユーザーが積極的に byte-compile-file hoge.el しないと、hoge.elc は、作成されない。まあ、pythonの余計なお節介とも解 釈できるな。

その後の調査で、リミット値はハード・コーディングされてる事が判明した。

eval.c:  max_lisp_eval_depth = 1600;

動いているemacsにgdbをアタッチして、書き換えちゃえ。

(gdb) p max_lisp_eval_depth
$1 = 1600
(gdb) set max_lisp_eval_depth = 3000
(gdb) c
Continuing.

リミッターが外れた暴走族。1000桁も計算できた。

Floating-point precision is 1000 digits
Working... sin = 0.499999999999999999999999999999999999999999999999999999999999\
  :
9999999999999999999999999999999999999999999999999999999999999999999999996

この技は自己責任ね。事故責任は自分に有りですよと、釘を刺すコメントが出 てた。

doc: /* Limit on depth in `eval', `apply' and `funcall' before error.

This limit serves to catch infinite recursions for you before they cause
actual stack overflow in C, which would be fatal for Emacs.
You can safely make it considerably larger than its default value,
if that proves inconveniently small.  However, if you increase it too far,
Emacs could overflow the real C stack, and crash.  */

無限再帰なんて語句が出てきてるけど、今実行してるmysinのコードには、再 帰が見当らないんですけど。何処かに深くなる再帰(精度に依存する)が潜んでいるんだな。

math-add, math-mul

前回からの続きで、calc中の重要な演算を見ていく。その為の軽いコードを作 成する。

どんなコードでもいいんだけど、先日に来日したGPUの雄であるnvidiaの社長 さんに敬意を表し、積和演算にする。なんたって社長さんはAIの研究所やらを 日本に作るって(リップ・サービス?)発言してたからね。

(defmath gpu (a b c)
  "Return a * b + c"
  (interactive 3 "gpu")
  (+ (* a b) c))

(defun calc-gpu nil "Return a * b + c"
       (interactive)
       (calc-slow-wrapper
        (calc-enter-result 3 "gpu"
                           (cons 'calcFunc-gpu
                                 (calc-top-list-n 3)))))

(defun calcFunc-gpu (a b c)
  "Return a * b + c"
  (math-add
   (math-mul a b)
   c))

defmathでgpuと言う3引数の関数を登録。それがcalcネイティブな2つの内部関 数として登録される。なお、この展開系の確認は、前回のmath-ppを利用した。

alg' gpu(3, 4, 5)
     17
alg' gpu(3, 4, 0)
     12
alg' gpu(1, 4, 5)
     9

代数式での実行。乗算だけをしたい場合、加算の単位元でる0を指定。加算だ けをしたい場合、乗算の単位元である1を指定。こんな具合にこの関数は、AI で多用される行列の計算の根本を支える演算だ。

今世界中で引っ張り凧のA200とか言うGPUは、こんな演算をめっぽう大量に高 速実行できる装置だ。これを社長さんは優先的に日本に供給してくれると言っ てるんだ。

次は、この演算の引数をスタックから取得するようにしてみる。ZDして、g gpu で登録。

    3
    4
    5
gpu>17

スタックに、3 4 5 を積んでから、zg で実行。

カンマ vs. スペース

gpu(3 4 5)                           ;; input
Wrong number of arguments: gpu(60)   ;; on mini-buffer
alg'>gpu(3 * 4 * 5)                  ;; on *Calc Trail*

こんなエラーを食ったぞと。

Q: lispの場合、(gpu 3 4 5) の様にスペース区切りで入力します。一方普通の言 語では、gpu(3, 4, 5) の様にカンマ区切りです。カンマよりスペースの方が 入力が容易と思えます。カンマが採用された理由は何ですか? 源流を辿って 3000文字で説明してください。

A: カンマとスペースの使い分けは、言語設計における様々な要因によるものです。Lispのスペース区切りと他の言語のカンマ区切りの違いには、歴史的、技術的、および言語設計上の理由が絡んでいます。

歴史的要因 カンマ区切りは、多くのプログラミング言語で関数の引数を区切るために採用されてきました。これは、数学的な表記法や自然言語でのリスト分割の慣習に由来しています。例えば、数学では f(x, y, z) のような関数呼び出しをカンマで引数を区切って表現します。このような慣習が、プログラミング言語における関数呼び出しの表記方法に影響を与えました。

一方、Lispは異なる考え方に基づいて設計されました。Lispでは、関数やオペレータはリストとして表され、その要素はスペースで区切られています。このスタイルは、Lispのデータ構造としてのリストと密接に結びついています。

技術的要因 カンマの使用は、構文解析において引数の明示的な区切りを提供することができます。カンマを使うことで、コンパイラやパーサーがより容易に関数の引数を識別し、解釈することができます。一方、スペース区切りの場合、解析器は単純にスペースを見つけてそこを区切りとして解釈するため、文脈によっては曖昧さが生じることがあります。

言語設計上の理由 言語設計者は、可読性、書きやすさ、一貫性などの要素を考慮して構文を決定します。カンマ区切りは、人間が視覚的に引数を区別しやすく、リストやタプルなどの要素を区別するのに役立ちます。一方、スペース区切りは、文法がシンプルで、解析が容易な場合がありますが、読み手によっては、引数間の区別が難しくなることがあります。

要するに、カンマやスペースの使用は、言語設計者がその言語の目的や哲学、文法の一貫性、技術的制約などを考慮して決定されます。それぞれの選択にはその言語が持つ特徴や背景が反映されています。

Q: 最初にカンマを使ったのは、fortranですか? それより古い言語でしょう か?

A: Fortranがカンマを引数の区切りとして使ったのは非常に古いですが、それよりも前の言語でカンマが使用された例もあります。

Fortranは、1957年に最初のバージョンがリリースされました。この言語は、数値計算や科学技術計算向けに設計されており、カンマを引数の区切りとして採用しました。しかし、Fortran以前の時代にも、カンマはコンピュータプログラミングにおいてリストの区切り記号として使われることがありました。

例えば、FORTRANよりも古いとされる言語として、Lispがあります。Lispは1958年に開発されましたが、Lispではリストの要素はスペースで区切られました。そのため、カンマを引数の区切りとして採用した最初の言語としてはFortranが知られています。

プログラミング言語における構文の採用は、その時代や文脈によって異なりますが、カンマが引数の区切りとして使われる慣習は古くから存在し、Fortranがその一例として挙げられます。

関連するかも知れないトリビア。

自動改札とロケットとFORTRAN

Q: アメリカではカンマとピリオドのどちらを使うのですか?

A: イギリスと米国では小数位を表すのにピリオドを使用しますが、その他の 多くの国々ではピリオドの代わりにコンマを使用しています。 小数位の区切 り文字は小数点とも呼ばれます。 同様に、イギリスと米国では千単位をコン マで区切るのに対し、他の国々では代わりにピリオドを用いたり、狭い空白文 字で区切ったりしています。

ドイツに駐留してた時、違和感があったぞ! ロケールを変更して、ドイツ流に 慣れにおこう > 若い諸君。

ドイツもこいつも、英米だけじゃないからね。

control of calc

calcのコントール・フローがどうなってるか? そりゃ、edebugのバックトレー ス・コマンド(d)の出番だな。知ったかぶりに書いているけど、マニュアルを 精査してて見つけたのさ。

木を見て森を見ずじゃ、どこかの学者さんみたいになっちゃうんで、先に知ら べて億。

math-mul でedebuggerに入いる様に設定。止ったら、すかさず d するだけ。

Edebug Backtrace at alg' gpu(3,4,5)

[sakae@fb /tmp]$ cat alg.log | cut -b-100
  math-mul(3 4)
  (math-add (math-mul a b) c)
  calcFunc-gpu(3 4 5)
  apply(calcFunc-gpu (3 4 5))
  (and (or (consp (car a)) (fboundp (car a)) (and (not (featurep 'calc-ext)) (require 'cal
  (if func (apply (cdr func) args) (and (or (consp (car a)) (fboundp (car a)) (and (not (f
  (or (and var-EvalRules (progn (or (eq var-EvalRules math-eval-rules-cache-tag) (progn (r
  :
  funcall-interactively(calc-algebraic-entry nil)
  call-interactively(calc-algebraic-entry nil nil)
  command-execute(calc-algebraic-entry)

Edebg Backtrace at 1.2 0.34 0.056 zg

[sakae@fb /tmp]$ cat zg.log | cut -b-90
  math-mul((float 12 -1) (float 34 -2))
  (math-add (math-mul a b) c)
  calcFunc-gpu((float 12 -1) (float 34 -2) (float 56 -3))
  apply(calcFunc-gpu ((float 12 -1) (float 34 -2) (float 56 -3)))
  (and (or (consp (car a)) (fboundp (car a)) (and (not (featurep 'calc-ext)) (require 'cal
  (if func (apply (cdr func) args) (and (or (consp (car a)) (fboundp (car a)) (and (not (f
  (or (and var-EvalRules (progn (or (eq var-EvalRules math-eval-rules-cache-tag) (progn (r
  :
  funcall-interactively(calc-gpu)
  call-interactively(calc-gpu nil nil)
  command-execute(calc-gpu)

どういう基準か知らないけど、S式の中身まで表示されてる。これでは全体像 を把握できないと思うので、S式を除外してみる。これが正しい道かどうかは、 分からんけど。edebuggerに、フレームを登り降りするコマンドが無いのは、 残念至極なんで、こんな事をしてみるのさ。

[sakae@fb /tmp]$ cat alg.log | grep -v '  (' | cut -b-90
  math-mul(3 4)
  calcFunc-gpu(3 4 5)
  apply(calcFunc-gpu (3 4 5))
  math-normalize((calcFunc-gpu 3 4 5))
  math-simplify((calcFunc-gpu 3 4 5))
  calc-normalize-fancy((calcFunc-gpu 3 4 5))
  calc-normalize((calcFunc-gpu 3 4 5))
  mapcar(calc-normalize ((calcFunc-gpu 3 4 5)))
  calc-alg-entry(nil)
  funcall((closure ((auto) (prefix)) nil (let ((calc-language (if prefix nil calc-language
  calc-do((closure ((auto) (prefix)) nil (let ((calc-language (if prefix nil calc-language
  calc-algebraic-entry(nil)
  funcall-interactively(calc-algebraic-entry nil)
  call-interactively(calc-algebraic-entry nil nil)
  command-execute(calc-algebraic-entry)
[sakae@fb /tmp]$ cat zg.log | grep -v '  (' | cut -b-90
  math-mul((float 12 -1) (float 34 -2))
  calcFunc-gpu((float 12 -1) (float 34 -2) (float 56 -3))
  apply(calcFunc-gpu ((float 12 -1) (float 34 -2) (float 56 -3)))
  math-normalize((calcFunc-gpu (float 12 -1) (float 34 -2) (float 56 -3)))
  math-simplify((calcFunc-gpu (float 12 -1) (float 34 -2) (float 56 -3)))
  calc-normalize-fancy((calcFunc-gpu (float 12 -1) (float 34 -2) (float 56 -3)))
  calc-normalize((calcFunc-gpu (float 12 -1) (float 34 -2) (float 56 -3)))
  calc-enter-result(3 "gpu" (calcFunc-gpu (float 12 -1) (float 34 -2) (float 56 -3))) 
  funcall((lambda nil (calc-enter-result 3 "gpu" (cons 'calcFunc-gpu (calc-top-list-n 3)))
  calc-do((lambda nil (calc-enter-result 3 "gpu" (cons 'calcFunc-gpu (calc-top-list-n 3))) 
  calc-gpu()
  funcall-interactively(calc-gpu)      
  call-interactively(calc-gpu nil nil) 
  command-execute(calc-gpu)  

debugger is good tool

edebugの代わりに、debuugerを使ってみる。debug-on-entry math-mul

Debugger entered--entering a function:
 * math-mul(3 4)
  (math-add (math-mul a b) c)
  calcFunc-gpu(3 4 5)
  apply(calcFunc-gpu (3 4 5))
  (and (or (consp (car a)) (fboundp (car a)) (and (not (featurep 'calc-ext)) (r$
  (if func (apply (cdr func) args) (and (or (consp (car a)) (fboundp
  (car a)) ($
   :

よく見ると、関数名の所に下線が引かれたものが有る。マニュアルによると、 emacsがソースの場所を知っている印らしい。(これで、上の想像は否定された)

下線の所にカーソルを持っていって、RETを叩くと、ソースが閲覧できる。お おいに嬉しいサービスだ。

command-executeにも下線が付いていたので、やってみると

(defun command-execute (cmd &optional record-flag keys special)
  ;; BEWARE: Called directly from the C code.
  "Execute CMD as an editor command.
CMD must be a symbol that satisfies the `commandp' predicate.
  :

simple.elが本拠地であった。

なお、フレームの移動には、n,p を使う。

+,- で、一行に畳まれたコードを、展開、圧縮できる。余り使わないだろうけ ど。v で、ローカル変数の値も確認できる。

b でフレームにマークを付けてから c すると、そこまで継続実行してくれる。 d だと、ステップ実行になる。

emacs for gdb

そんな事が出来るやつを作ろう。お手本は、ports/editor/emacs/Makefile

.if ${FLAVOR} == "no_x11"
CONFIGURE_ARGS+=        --without-x \
                        --without-dbus \
                        --without-gsettings \
                        --without-harfbuzz \
                        --without-jpeg \
                        --without-lcms2 \
                        --without-libotf \
                        --without-m17n-flt

この設定の頭に、–prefix=/opt ってのを prefix したよ。最近は、考える事 を放棄してるな。gmake -j2 で、わずか3分待つのみ。configureの方が、時間 がかかるんではなかろうか。

vbox$ gdb -q -p 36796 /opt/bin/emacs-29.1
Reading symbols from /opt/bin/emacs-29.1...
  :
(gdb) bt
 :
#11 0x162b7a61 in read_key_sequence (keybuf=<optimized out>,
    prompt=<optimized out>, dont_downcase_last=<optimized out>,
    can_return_switch_frame=<optimized out>,
    fix_current_buffer=<optimized out>, prevent_redisplay=<optimized out>)
    at keyboard.c:10083
#12 0x162b5b97 in command_loop_1 () at keyboard.c:1384
#13 0x1634cb26 in internal_condition_case (bfun=0x162b5780 <command_loop_1>,
    handlers=0x48, hfun=0x162b6e00 <cmd_error>) at eval.c:1474
#14 0x162b576a in command_loop_2 (handlers=0x48) at keyboard.c:1133
#15 0x1634c3a4 in internal_catch (tag=0x6d80,
    func=0x162b5730 <command_loop_2>, arg=0x48) at eval.c:1197
#16 0x162b4fa1 in command_loop () at keyboard.c:1111
#17 0x162b4e34 in recursive_edit_1 () at keyboard.c:720
#18 0x162b513f in Frecursive_edit () at keyboard.c:803
#19 0x162b4084 in main (argc=<optimized out>, argv=0xcf7e36c4) at emacs.c:2529

調子こいて、FreeBSDにも導入。ソース、バイナリーの一式で500Mだった。遊 びがいが有りそうだな。

日経Linuxの最終号を記念に購入したけど、もう無いのか。残るはSDだけになっ てしまったな。後はpython一色で、とてもつまらない世になってしまった。

なんか、emacsのソースをちら見してるんだけど、難解そうで面白いぞ。

[sakae@fb /opt/src/emacs-29.1/src]$ wc *.[ch] | sort -nr | head
  494146 1909506 15325698 total
   37709  156041 1224000 xdisp.c
   31708  103676  929801 xterm.c
   13333   53260  420039 keyboard.c
   12312   42612  343719 image.c
   12252   44993  370853 coding.c
   11358   44264  370207 w32fns.c
   11192   37865  310873 w32.c
   10270   36132  316571 xfns.c
    9218   73718  479218 macuvs.h

さしあたって見て桶ってファイルは、keyboard.c だな。

debugger,edebugを使った時にフレームの最上位に、 command_execute が居 た。これとの接点になる部分を探してみると、keyboard.cに行き付く。

with display-line-numbers-mode

 12386   DEFSYM (Qcommand_execute, "command-execute");
 1503             call1 (Qcommand_execute, Vthis_command);
3025       call4 (Qcommand_execute, tem, Qnil, Fvector (1, &last_input_event), Qt);

M-x calc すると

#0  command_loop_1 () at keyboard.c:1503
#1  0x0056ab73 in internal_condition_case (bfun=0x4eadb0 <command_loop_1>,
    handlers=0x48, hfun=0x4ec070 <cmd_error>) at eval.c:1474
#2  0x004eada0 in command_loop_2 (handlers=0x48) at keyboard.c:1133
#3  0x0056a460 in internal_catch (tag=0x6b70, func=0x4ead80 <command_loop_2>,
    arg=0x48) at eval.c:1197
#4  0x004ea6e5 in command_loop () at keyboard.c:1111
#5  0x004ea5e1 in recursive_edit_1 () at keyboard.c:720
#6  0x004ea8a5 in Frecursive_edit () at keyboard.c:803
#7  0x004e96b1 in main (argc=<optimized out>, argv=0xffbfeb24) at emacs.c:2529

どうもOSっぽい動きをしてるな。キーボード割り込みね。

なお、BPをセットする時、emacsで起動したものだと、セットできないと文句 を言われる。 "Cannot Insert Breakpoints" エラーとは

[sakae@fb /tmp]$ ls -l /opt/bin/emacs*
lrwxr-xr-x  1 sakae  wheel        10 Dec 11 06:16 /opt/bin/emacs@ -> emacs-29.1
-rwxr-xr-x  1 sakae  wheel  12634324 Dec 11 06:16 /opt/bin/emacs-29.1*
-rwxr-xr-x  1 sakae  wheel     83040 Dec 11 06:16 /opt/bin/emacsclient*

リンク・ファイルじゃ駄目で、正式名称だと大丈夫。まあ、そういう事だと不 承不承に承知しておきましょ。いや今後のオペレーションを考えて、ln emacs-29.1 emacs しときましょ。

上で、call1とかが出てきている。lisp.hに定義されてた。

/* Call function fn with 1 argument arg1.  */
INLINE Lisp_Object
call1 (Lisp_Object fn, Lisp_Object arg1)
{
  return CALLN (Ffuncall, fn, arg1);
}

/* Call a function F that accepts many args, passing it ARRAY's elements.  */
#define CALLMANY(f, array) (f) (ARRAYELTS (array), array)

/* Call a function F that accepts many args, passing it the remaining args,
   E.g., 'return CALLN (Fformat, fmt, text);' is less error-prone than
   '{ Lisp_Object a[2]; a[0] = fmt; a[1] = text; return Fformat (2, a); }'.
   CALLN requires at least one function argument (as C99 prohibits
   empty initializers), and is overkill for simple usages like
   'Finsert (1, &text);'.  */
#define CALLN(f, ...) CALLMANY (f, ((Lisp_Object []) {__VA_ARGS__}))

eval.c

DEFUN ("funcall", Ffuncall, Sfuncall, 1, MANY, 0,
       doc: /* Call first argument as a function, passing remaining arguments to it.
Return the value that function returns.
Thus, (funcall \\='cons \\='x \\='y) returns (x . y).
usage: (funcall FUNCTION &rest ARGUMENTS)  */)
  (ptrdiff_t nargs, Lisp_Object *args)
{
  specpdl_ref count;
    :

この中で、quitやらgcのチャンスを確認。更には深い再帰でヤバくなっていな いか確認しながら、指示された関数を処理してる。editorは何が何でも、落ち る事には、敏感で、色々とチェックしてるぞ。

どこかの銀行屋みたいに、慢心してないで、対処してるな。


This year's Index

Home