10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ビル・ゲイツさんが約50年前に書いた「6502 BASIC」をAI(Geminiさん)の助けを借りてGo言語で再現してみた

Posted at

まえがき

去年、Microsoftが「Microsoft BASIC for 6502 Microprocessor - Version 1.1」のソースコードを2025年9月3日にオープンソース化したという記事をみてずっと気になっていました。
はたして、あのビル・ゲイツさんがどのようなコードを書いたのか知りたかったのです。

ソースコードは、

にあるので誰でもみられます。

プログラムの馴染みのないアセンラーのソースコードだけを見ていてもどんな動きをするのか、さっぱり動きのイメージがつきません。動かしてみるのが一番です。
そこで、AI(GooleのGeminiさん)の助けを借りてGo言語に移植して動かしてみた実験を紹介します。

m6502.asm のGeminiさんによる分析要約

Geminiさんに分析してもらいました。

m6502.asm は、1976年に Microsoft が開発した MOS 6502 プロセッサ向け Microsoft BASIC (8K Version 1.1) のアセンブリソースコードです。Apple II, Commodore PET, KIM-1 などの初期パーソナルコンピュータの礎となった歴史的なプログラムです。

1. 概要

  • 開発: MICRO-SOFT (1976年)
  • ターゲット: MOS 6502 CPU
  • 機能: BASIC 言語のインタプリタ(入力、編集、実行)

2. システム構成と特徴

コードは条件付きアセンブルを多用し、単一ソースで複数のハードウェアに対応しています。

  • プラットフォーム切り替え (REALIO):
    • 0: PDP-10 シミュレータ
    • 1: KIM-1
    • 2: OSI
    • 3: Commodore
    • 4: Apple II
  • メモリ管理:
    • ゼロページ (Page 0): ポインタ、フラグ、浮動小数点アキュムレータ (FAC) を配置し、6502の高速なアドレッシングモードを最大限活用
    • 動的配置: プログラムテキスト、単純変数、配列、文字列スペースをメモリ上に効率的に配置
  • 主要ルーチン:
    • CHRGET: 次の文字を読み取り解析する核心ルーチン
    • CRUNCH: 予約語(PRINT, GOTO等)を1バイトのトークンに圧縮
    • FRMEVL: 演算子の優先順位を考慮した数式評価
    • GARBAGE COLLECTION: 断片化した文字列メモリの自動整理

3. 技術的なポイント

  • トークン化: メモリ節約のため、コマンドを数値(トークン)として保持
  • 浮動小数点演算: 当時のマイコンとしては高度な数学パッケージを搭載
  • 移植性: I/O ルーチンを抽象化し、ハードウェア依存部分を最小限に分離

このコードは、極めて限られた計算資源の中で、実用的なプログラミング環境を実現するための最適化技術の結晶と言えます。

とのことです。

エミュレータで動かすのは失敗

ソースコードの概要はわかったのですが、動かしてみないとイメージがつかめません。Geminiさんに聞くと、このソースコード自体をビルドして動かすのは難しいが、ビルド可能な6502 BASICが

にあって、Mac OSならば

brew install cc65
git clone https://github.com/mist64/msbasic.git
cd msbasic
chmod +x make.sh
./make.sh

ビルドまでは快調にできました。
エミュレータやシミュレータをいろいろ紹介してくれました。
https://www.scullinsteel.com/apple2/
https://www.masswerk.at/6502/

や、pythonで動作するpy65です。

残念ながら、どのエミュレータでも思ったような動きをしませんでした。失敗です。

Go言語に移植して動きをみる

AIは言語間の移植が得意だと思ってGemini CLIさんに

m6502.asmをGo言語に移植して

と頼んでみました。
そうすると、何となく移植してくれました。
いろいろエラーがありましたが、

修正して動くようにしたソースコード(クリックして展開できます。)
package main

import (
	"bufio"
	"encoding/binary"
	"fmt"
	"os"
	"strconv"
	"strings"
	// "unicode" // 未使用のため削除
)

// ==========================================
// 定数と設定
// ==========================================

const (
	BufLen  = 72
	MemSize = 0xffff
)

// ゼロページとメモリレイアウトのシミュレーション
// 0x0000 - 0x00FF: ゼロページ (構造体内でシミュレート)
// 0x0100 - 0x01FF: スタック
// 0x0200 - 0x02FF: 入力バッファ (BUF)
// 0x0400 ...     : プログラムテキスト (TXTTAB)
const (
	AddrBuf    uint16 = 0x0200
	AddrTxtTab uint16 = 0x0400 // BASICプログラムの開始位置
)

// トークン (予約語) m6502.asm (REALIO=4 設定) に基づく
const (
	TkEnd     = 0x80
	TkFor     = 0x81
	TkNext    = 0x82
	TkData    = 0x83
	TkInput   = 0x84
	TkDim     = 0x85
	TkRead    = 0x86
	TkLet     = 0x87
	TkGoto    = 0x88
	TkRun     = 0x89
	TkIf      = 0x8A
	TkRestore = 0x8B
	TkGosub   = 0x8C
	TkReturn  = 0x8D
	TkRem     = 0x8E
	TkStop    = 0x8F
	TkOn      = 0x90
	TkNull    = 0x91
	TkWait    = 0x92
	TkDef     = 0x93
	TkPoke    = 0x94
	TkPrint   = 0x95
	TkCont    = 0x96
	TkList    = 0x97
	TkClear   = 0x98
	TkGet     = 0x99
	TkNew     = 0x9A
	TkTab     = 0x9B
	TkTo      = 0x9C
	TkFn      = 0x9D
	TkSpc     = 0x9E
	TkThen    = 0x9F
	TkNot     = 0xA0
	TkStep    = 0xA1
	TkPlus    = 0xA2
	TkMinus   = 0xA3
	TkMul     = 0xA4
	TkDiv     = 0xA5
	TkPow     = 0xA6
	TkAnd     = 0xA7
	TkOr      = 0xA8
	TkGt      = 0xA9
	TkEq      = 0xAA
	TkLt      = 0xAB
	TkSgn     = 0xAC
	TkInt     = 0xAD
	TkAbs     = 0xAE
	TkUsr     = 0xAF
	TkFre     = 0xB0
	TkPos     = 0xB1
	TkSqr     = 0xB2
	TkRnd     = 0xB3
	TkLog     = 0xB4
	TkExp     = 0xB5
	TkCos     = 0xB6
	TkSin     = 0xB7
	TkTan     = 0xB8
	TkAtn     = 0xB9
	TkPeek    = 0xBA
	TkLen     = 0xBB
	TkStr     = 0xBC
	TkVal     = 0xBD
	TkAsc     = 0xBE
	TkChr     = 0xBF
	TkLeft    = 0xC0
	TkRight   = 0xC1
	TkMid     = 0xC2
	TkGo      = 0xC3 // GO TO 用に使用
)

// 解析用トークンマップ (CRUNCH)
var strToToken = map[string]byte{
	"END": TkEnd, "FOR": TkFor, "NEXT": TkNext, "DATA": TkData, "INPUT": TkInput,
	"DIM": TkDim, "READ": TkRead, "LET": TkLet, "GOTO": TkGoto, "RUN": TkRun,
	"IF": TkIf, "RESTORE": TkRestore, "GOSUB": TkGosub, "RETURN": TkReturn,
	"REM": TkRem, "STOP": TkStop, "ON": TkOn, "NULL": TkNull, "WAIT": TkWait,
	"DEF": TkDef, "POKE": TkPoke, "PRINT": TkPrint, "CONT": TkCont, "LIST": TkList,
	"CLEAR": TkClear, "GET": TkGet, "NEW": TkNew, "TAB(": TkTab, "TO": TkTo,
	"FN": TkFn, "SPC(": TkSpc, "THEN": TkThen, "NOT": TkNot, "STEP": TkStep,
	"+": TkPlus, "-": TkMinus, "*": TkMul, "/": TkDiv, "^": TkPow,
	"AND": TkAnd, "OR": TkOr, ">": TkGt, "=": TkEq, "<": TkLt,
	"SGN": TkSgn, "INT": TkInt, "ABS": TkAbs, "USR": TkUsr, "FRE": TkFre,
	"POS": TkPos, "SQR": TkSqr, "RND": TkRnd, "LOG": TkLog, "EXP": TkExp,
	"COS": TkCos, "SIN": TkSin, "TAN": TkTan, "ATN": TkAtn, "PEEK": TkPeek,
	"LEN": TkLen, "STR$": TkStr, "VAL": TkVal, "ASC": TkAsc, "CHR$": TkChr,
	"LEFT$": TkLeft, "RIGHT$": TkRight, "MID$": TkMid, "GO": TkGo,
}

// リスト表示用トークンマップ (LIST)
var tokenToStr = map[byte]string{}

func init() {
	for s, t := range strToToken {
		tokenToStr[t] = s
	}
	// 部分一致や記号を含むキーワードの特別な処理
	tokenToStr[TkTab] = "TAB("
	tokenToStr[TkSpc] = "SPC("
}

// ==========================================
// インタープリタの状態
// ==========================================

type Interpreter struct {
	Memory [MemSize]byte

	// ゼロページポインタ
	TxtPtr uint16 // 現在のテキストへのポインタ
	CurLin uint16 // 現在の行番号 (0xFFFF = ダイレクトモード)
	VarTab uint16 // 変数の開始位置
	AryTab uint16 // 配列の開始位置
	StrEnd uint16 // 変数/配列の終了位置 (このプロトタイプではプログラムコードの終了位置も兼ねる)
}

func NewInterpreter() *Interpreter {
	i := &Interpreter{}
	// メモリマップの初期化
	i.VarTab = AddrTxtTab
	i.AryTab = AddrTxtTab
	i.StrEnd = AddrTxtTab

	// TXTTAB を 0x0000 (プログラム終了) で初期化
	// ReLink 関数がこのマーカーを適切に設定します。
	i.Memory[AddrTxtTab] = 0
	i.Memory[AddrTxtTab+1] = 0

	return i
}

// ==========================================
// コアメソッド
// ==========================================

// ChrGet : 次の文字を読み込み、TxtPtr をインクリメントします。
// 戻り値: 文字, isEnd(: または 0), isNumeric(数値かどうか)
func (i *Interpreter) ChrGet() (byte, bool, bool) {
	if int(i.TxtPtr) >= MemSize { // MemSize 定数とのオーバーフロー警告を避けるため int にキャストしてチェック
		return 0, true, false
	}
	i.TxtPtr++
	ch := i.Memory[i.TxtPtr]

	isEnd := (ch == 0 || ch == ':')
	isNum := (ch >= '0' && ch <= '9')

	if ch == ' ' {
		return i.ChrGet() // スペースをスキップ
	}
	return ch, isEnd, isNum
}

// MainLoop : REPL (Read-Eval-Print Loop)
func (i *Interpreter) MainLoop() {
	reader := bufio.NewReader(os.Stdin)
	fmt.Println("BASIC M6502 GO PORT (PARTIAL)")
	fmt.Println("READY.")

	for {
		fmt.Print("> ")
		line, _ := reader.ReadString('\n')
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		// 1. クランチ (トークン化) してバッファへ
		tokenized := i.Crunch(line)

		// 2. 行番号のチェック
		lineNum, afterNumIdx, hasLineNum := i.ParseLineNumber(tokenized)

		if hasLineNum {
			// プログラムモード: 行を保存
			i.StoreLine(lineNum, tokenized[afterNumIdx:])
		} else {
			// ダイレクトモード: 実行
			// シミュレーションの一貫性のために、トークン化された行をメモリ内の BUF 領域にコピー
			copy(i.Memory[AddrBuf:], tokenized)
			i.Memory[AddrBuf+uint16(len(tokenized))] = 0

			i.TxtPtr = AddrBuf - 1
			i.CurLin = 0xFFFF // ダイレクトモードフラグ
			i.Execute()
			fmt.Println("\nREADY.")
		}
	}
}

// Crunch : 単純なトークナイザ
func (i *Interpreter) Crunch(input string) []byte {
	var out []byte
	inputLen := len(input)

	for idx := 0; idx < inputLen; {
		// 引用符で囲まれた文字列のチェック (引用符内はトークン化しない)
		if input[idx] == '"' {
			out = append(out, input[idx])
			idx++
			for idx < inputLen && input[idx] != '"' {
				out = append(out, input[idx])
				idx++
			}
			if idx < inputLen {
				out = append(out, input[idx]) // 閉じ引用符
				idx++
			}
			continue
		}

		// キーワードのマッチングを試行
		matched := false
		// マップを反復処理 (順序はランダム) するため、これは素朴な実装です。
		// 本格的な実装では、最長一致を最初にチェックすべきです。

		// 単純なアプローチ: 全てのキーワードと照合
		// ASM では、RESLST テーブルを順次チェックしています。
		for k, v := range strToToken {
			if strings.HasPrefix(strings.ToUpper(input[idx:]), k) {
				// GO TO の特別なチェック
				if k == "GO" && idx+4 <= inputLen && strings.ToUpper(input[idx:idx+4]) == "GO TO" {
					out = append(out, TkGoto)
					idx += 5 // "GO TO" をスキップ (スペース除去ロジックが必要? ASM は "GO TO" を "GOTO" として扱います)
					matched = true
					break
				}

				// 標準的なマッチ
				out = append(out, v)
				idx += len(k)
				matched = true
				break
			}
		}

		if !matched {
			out = append(out, input[idx])
			idx++
		}
	}
	out = append(out, 0) // NULL 終端
	return out
}

func (i *Interpreter) ParseLineNumber(buf []byte) (uint16, int, bool) {
	// buf は [トークン, トークン, ...] を含む
	// 行番号は先頭の ASCII 数字

	idx := 0
	digits := ""
	for idx < len(buf) && buf[idx] >= '0' && buf[idx] <= '9' {
		digits += string(buf[idx])
		idx++
	}

	if digits == "" {
		return 0, 0, false
	}

	ln, err := strconv.Atoi(digits)
	if err != nil {
		// 上記のチェックによりあり得ないはずだがエラー処理
		return 0, 0, false
	}
	// 行番号後のスペースをスキップ (クランチ処理で保存/削除されている可能性あり)
	for idx < len(buf) && buf[idx] == ' ' {
		idx++
	}

	return uint16(ln), idx, true
}

// StoreLine : リンクされたリスト構造のプログラムメモリに行を 挿入/置換/削除 します
func (i *Interpreter) StoreLine(lineNum uint16, payload []byte) {
	// メモリ内の行の構造:
	// [LinkL][LinkH] [LineL][LineH] [Payload...] [00]

	current := AddrTxtTab

	// 1. 位置を探す
	for {
		// current がプログラム終了マーカーを指している場合
		if i.Memory[current] == 0 && i.Memory[current+1] == 0 {
			break
		}

		link := binary.LittleEndian.Uint16(i.Memory[current:])
		ln := binary.LittleEndian.Uint16(i.Memory[current+2:])
		if ln >= lineNum {
			break // 挿入ポイントまたは既存の行が見つかった
		}
		current = link // 次の行へ移動
	}

	// current は調査中の位置 (終了マーカー、完全一致、または次の大きな行番号) を指す

	// 完全一致 (行番号が存在する) の場合、まず古い行を削除
	if binary.LittleEndian.Uint16(i.Memory[current+2:]) == lineNum && i.Memory[current] != 0 {
		nextLink := binary.LittleEndian.Uint16(i.Memory[current:])
		i.DeleteLineBlock(current, nextLink)
	}

	// ペイロードが空の場合 (例: "10" とだけ入力された)、行を削除することを意味する。
	if len(payload) == 1 && payload[0] == 0 {
		i.ReLink() // 削除が発生した場合は再リンク
		return
	}

	// `current` に新しい行を挿入
	lineSize := uint16(2 + 2 + len(payload)) // Link (2) + LineNum (2) + Data (payload は null を含む)
	i.MakeSpace(current, lineSize)

	// 新しい行のヘッダとペイロードを書き込む
	binary.LittleEndian.PutUint16(i.Memory[current:], 1) // ReLink が継続するようにダミーのリンクを設定
	binary.LittleEndian.PutUint16(i.Memory[current+2:], lineNum)
	copy(i.Memory[current+4:], payload)

	i.ReLink() // メモリ変更後にすべてのリンクを再構築
}

func (i *Interpreter) MakeSpace(addr uint16, size uint16) {
	// addr からメモリを size 分だけ上にシフトする。
	// `i.StrEnd` はプログラムコードセグメントの終わりを示す。
	// `i.StrEnd` から `addr` までをシフトする。

	if i.StrEnd+size >= MemSize {
		fmt.Println("OUT OF MEMORY ERROR")
		return // または、希望するエラー処理に応じて panic など
	}

	// 既存のデータを上に移動してスペースを確保
	for p := i.StrEnd; p >= addr; p-- {
		// 安全チェック: p が小さい場合の 0 ラップアラウンドを防止
		if p < addr { // これにより、p が addr を下回った場合にループが終了することを保証
			break
		}
		i.Memory[p+size] = i.Memory[p]
	}
	i.StrEnd += size // プログラムコードの終了位置を更新
}

func (i *Interpreter) DeleteLineBlock(start, end uint16) {
	// `end` から `start` へメモリを下に移動
	// `start` は削除される行のアドレス。
	// `end` は次の行 (またはプログラム終了マーカー) のアドレス
	size := end - start // 削除されるブロックのサイズ

	if start >= i.StrEnd { // 削除するものがない、または無効な範囲
		return
	}

	// データを下にシフトして削除されたブロックを上書き
	for p := start; p < i.StrEnd; p++ {
		if int(p)+int(size) < MemSize { // MemSize を超えて読み込まないようにする
			i.Memory[p] = i.Memory[p+size]
		} else {
			i.Memory[p] = 0 // 残りをゼロで埋める
		}
	}
	i.StrEnd -= size // プログラムコードの終了位置を更新
}

// ReLink : メモリ変更後にすべてのプログラム行のリンクを再構築します。
func (i *Interpreter) ReLink() {
	current := AddrTxtTab
	var prevLineStart uint16 = 0 // 前の行の *開始* アドレス

	for {
		// current がプログラム終了マーカー (00 00) を指している場合
		if current >= MemSize-1 || (i.Memory[current] == 0 && i.Memory[current+1] == 0) {
			// これはプログラム終了マーカーです。
			// 前の行のリンクはこのアドレスを指すべきです。
			if prevLineStart != 0 { // 前の行があった場合
				binary.LittleEndian.PutUint16(i.Memory[prevLineStart:], current)
			}
			break // プログラム終了
		}

		// 行の構成: [LinkL][LinkH] [LineL][LineH] [Payload...] [00]
		// 現在の行のテキストの NULL 終端を探す
		payloadStart := current + 4
		payloadEnd := payloadStart
		for payloadEnd < MemSize && i.Memory[payloadEnd] != 0 {
			payloadEnd++
		}

		if payloadEnd >= MemSize { // 安全チェック
			fmt.Println("Error: ReLink detected program overflow during scan.")
			break
		}

		nextLineStart := payloadEnd + 1

		// 前の行のリンクフィールドを現在の行を指すように更新
		if prevLineStart != 0 {
			binary.LittleEndian.PutUint16(i.Memory[prevLineStart:], current)
		}

		prevLineStart = current // 次の反復のために現在の行の開始アドレスを記憶
		current = nextLineStart // 計算された次の行の開始位置へ移動
	}
	// 最終的なプログラム終了マーカーが正しく配置され、ゼロクリアされていることを確認
	if current < MemSize-1 {
		i.Memory[current] = 0
		i.Memory[current+1] = 0
	}
}

// Execute : TxtPtr から始まるコードを実行します
func (i *Interpreter) Execute() {
	for {
		ch, isEnd, _ := i.ChrGet()
		if isEnd && ch == 0 {
			// 行末。
			if i.CurLin == 0xFFFF { // ダイレクトモード終了
				return
			}
			// プログラムモード: 行のリンクを使用して次の行に進む
			// 現在の TxtPtr は 00 終端を指している。
			// リンクは (現在の行の開始アドレス) にある
			// したがって、現在の行の開始を見つけ、そのリンクを使用する。

			// このロジックには欠陥があります。TXTPTR は実際のコードバイトを進みます。
			// リンクは次の行の *開始* を見つけるために使用されます。
			// `Execute` ループは行構造を認識する必要があります。
			// 行が終わる (ch == 0) とき、終了したばかりの行のリンクフィールドで指定された
			// アドレスにジャンプする必要があります。

			// 簡略化された `Execute` フロー:
			// 1. 現在の命令/トークンを取得。
			// 2. それを実行。
			// 3. 行末 (00 または :) の場合、次の *論理* 行/ステートメントに進む。
			//    行末 (00) の場合、現在の行の *開始* からリンクを取得。

			// これには `FindLineStart(addr)` ユーティリティが必要です。
			// 今のところ、TxtPtr は 00 の後にあると仮定します。つまり、次の論理行のリンクフィールドにいます。
			// ChrGet 内の `i.TxtPtr++` の後、`i.TxtPtr` は `payloadEnd+1` になります。

			currLineStart := i.FindLineStartByAddress(i.TxtPtr) // 現在の行の開始を取得
			if currLineStart == 0 {                             // 見つからないかエラー
				return // プログラム終了またはエラー
			}

			// リンクは currLineStart にある。
			nextLineStart := binary.LittleEndian.Uint16(i.Memory[currLineStart:])
			if nextLineStart == 0 || (i.Memory[nextLineStart] == 0 && i.Memory[nextLineStart+1] == 0) { // 現在の行のリンクが 0 または終了マーカーを指している場合
				return // プログラム実行終了
			}

			// TxtPtr を次の行のペイロード開始位置に設定
			i.TxtPtr = nextLineStart + 4 - 1                                  // -1 は ChrGet がすぐにインクリメントするため
			i.CurLin = binary.LittleEndian.Uint16(i.Memory[nextLineStart+2:]) // 現在の行番号を更新
			continue                                                          // 新しい行から実行を継続
		}

		if isEnd && ch == ':' {
			// 同じ行の次のステートメント。TxtPtr はすでに次のステートメントの最初の文字にある。
			continue
		}

		// トークンのディスパッチ
		switch ch {
		case TkPrint:
			i.CmdPrint()
		case TkList:
			i.CmdList()
		case TkNew:
			i.CmdNew()
		case TkRun:
			i.CmdRun()
		case TkEnd:
			return
		case TkGoto:
			i.CmdGoto()
		default:
			// ダイレクト文字として扱う (例: クランチされていない式や文字列内)
			// プロトタイプでは無視
			// fmt.Printf("Unknown token/char: %02X\n", ch)
		}
	}
}

// FindLineStartByAddress は、指定されたアドレスを含む行の開始アドレスを見つけます。
// これは、実行中に 1 つの行から次の行に移動するときにリンクを取得するために必要です。
func (i *Interpreter) FindLineStartByAddress(addr uint16) uint16 {
	curr := AddrTxtTab
	for {
		if curr >= MemSize-1 || (i.Memory[curr] == 0 && i.Memory[curr+1] == 0) {
			return 0 // プログラム終了
		}

		nextLineStart := binary.LittleEndian.Uint16(i.Memory[curr:]) // 現在の行のリンクを読み取る

		if nextLineStart == 0 { // リンクが 0 なら、これが最後の実際の行
			return curr
		}

		if addr >= curr && addr < nextLineStart {
			return curr // アドレスはこの行内にある
		}

		curr = nextLineStart // 次の行へ移動
	}
}

// Commands (コマンド)

func (i *Interpreter) CmdNew() {
	i.Memory[AddrTxtTab] = 0
	i.Memory[AddrTxtTab+1] = 0
	i.StrEnd = AddrTxtTab + 2 // プログラム終了マーカー位置をリセット
}

func (i *Interpreter) CmdList() {
	curr := AddrTxtTab
	for {
		link := binary.LittleEndian.Uint16(i.Memory[curr:])
		if link == 0 && curr == AddrTxtTab { // 空のプログラム
			return
		}
		if link == 0 { // プログラム終了マーカー
			break
		}

		ln := binary.LittleEndian.Uint16(i.Memory[curr+2:])

		fmt.Printf("%d ", ln)

		// トークンをデトークン化して表示
		p := curr + 4
		for {
			ch := i.Memory[p]
			if ch == 0 {
				break
			}

			if ch >= 0x80 {
				if s, ok := tokenToStr[ch]; ok {
					fmt.Print(s)
				} else {
					fmt.Printf("{%02X}", ch) // 未知のトークンのフォールバック
				}
			} else {
				fmt.Printf("%c", ch)
			}
			p++
		}
		fmt.Println()
		curr = link
	}
}

func (i *Interpreter) CmdRun() {
	// 最初の行から開始
	if i.Memory[AddrTxtTab] == 0 && i.Memory[AddrTxtTab+1] == 0 { // 空のプログラム
		return
	}

	i.TxtPtr = AddrTxtTab + 4 // 最初の行のテキストは Link (2) と LineNum (2) の後から始まる
	i.CurLin = binary.LittleEndian.Uint16(i.Memory[AddrTxtTab+2:])
	i.TxtPtr-- // ChrGet が最初のトークンにインクリメントするように戻す
	i.Execute()
}

func (i *Interpreter) CmdGoto() {
	// 行番号を解析
	// TxtPtr は現在 GOTO トークンの後にある (ChrGet は既に呼び出されている)

	// 数値を読み取る
	lineNum := 0
	for {
		ch, isEnd, isNum := i.ChrGet()
		if isEnd || !isNum { // ステートメントの終わりか非数字で停止
			i.TxtPtr-- // 非数字/終端を戻す
			break
		}
		lineNum = lineNum*10 + int(ch-'0')
	}

	// 行を探す
	curr := AddrTxtTab
	found := false
	for {
		// current がプログラム終了マーカーを指している場合
		if i.Memory[curr] == 0 && i.Memory[curr+1] == 0 {
			break
		}

		link := binary.LittleEndian.Uint16(i.Memory[curr:])
		ln := binary.LittleEndian.Uint16(i.Memory[curr+2:])
		if int(ln) == lineNum {
			// 見つかった
			i.CurLin = ln
			i.TxtPtr = curr + 4 - 1 // ChrGet 用に -1
			found = true
			break
		}
		curr = link
	}

	if !found {
		fmt.Printf("UNDEF'D STATEMENT IN %d\n", i.CurLin)
		i.TxtPtr = 0xFFFF // Execute ループを停止するために無効なアドレスを設定
		i.CurLin = 0      // 現在の行をリセット
	}
}

func (i *Interpreter) CmdPrint() {
	// 単純な文字列/式プリンタ
	// "STRING", 式 (未実装), ; をサポート

	newline := true

	for {
		// TxtPtr を進めずに次の文字を覗き見る (アセンブリでの peek をシミュレート)
		nextCh := i.Memory[i.TxtPtr+1]

		if nextCh == 0 || nextCh == ':' { // 行末またはステートメント末
			// Execute ループの ChrGet がこれを消費し、分岐を処理します
			break
		}

		ch, isEnd, _ := i.ChrGet() // ここで実際に消費
		if isEnd && ch == 0 {      // peek で捕捉されるはずだが念のため
			break
		}

		if ch == '"' {
			// 文字列リテラル
			for {
				ch2, isEnd2, _ := i.ChrGet()
				if isEnd2 || ch2 == '"' {
					break
				}
				fmt.Printf("%c", ch2)
			}
		} else if ch == ';' {
			newline = false
		} else {
			// 式評価のプレースホルダー
			fmt.Printf("[EXPR %c]", ch) // 今のところ、文字列以外は文字をそのまま表示
		}
	}

	if newline {
		fmt.Println()
	}
}

func main() {
	i := NewInterpreter()
	i.MainLoop()
}


のような感じになります。
これならGo言語の得意な人なら、何やっているのかわかると思います。
ビル・ゲイツさんの工夫もわかるような気がします。

このプログラムを動かすと

image.png

のような感じです。
動きは

2026-01-24_05-51-58 (1).gif

のようなイメージです。

大学の時(45年ぐらい前)BASICでプログラミングしていた時に、こんな感じだったような気がします。

移植したGo言語のコードの状態

移植したGo言語のコードをGeminiさんに解説してもらいました。

m6502_basic.go は m6502.asm の完全な移植ではありません。

Goのコードは、オリジナルのアセンブリコードの構造(メモリレイアウトやトークン値など)を模倣し始めたばかりのプロトタイプまたは部分的
な移植です。

以下に主な違いと未実装部分をまとめます。

  1. 明示的な記述
    m6502_basic.go の MainLoop 関数内の出力やコメントに、はっきりと "PARTIAL"(部分的) であることが示されています。
    1 fmt.Println("BASIC M6502 GO PORT (PARTIAL)")

  2. 実装されている機能
    Go版で実装されているのは、インタプリタの「外側」の枠組みのみです。

  • REPL (Read-Eval-Print Loop): 入力を受け取り、解析するループ
  • メモリ管理: BASICプログラムの行をリンクリスト形式でメモリに格納・削除・挿入するロジック (StoreLine, ReLink など)
  • トークナイザ (Crunch): テキストを予約語トークン(TkPrint や TkGoto など)に変換する処理
  • ごく一部のコマンド: NEW, LIST, RUN, GOTO, PRINT (ただし PRINT は文字列リテラルのみで計算式は不可), END
  1. 未実装の主要機能
    m6502.asm にある中核機能のほとんどが欠けています。
  • 式評価 (Expression Evaluator): 数式(1 + 2 * 3など)や論理式を計算する FRMEVL や EVAL に相当する機能がありません。CmdPrint
    内には // 式評価のプレースホルダー というコメントがあり、未実装であることがわかります
  • 変数管理: 変数テーブル (VARTAB) や配列 (ARYTAB)、変数の代入 (LET)、参照 (PTRGET) の仕組みがありません
  • 制御構文: FOR / NEXT, IF / THEN, GOSUB / RETURN, ON ... GOTO, DATA / READ などが実装されていません
  • 数学関数: SIN, COS, TAN, SQRT などの浮動小数点演算ライブラリ(Math-Pak)が含まれていません
  • 文字列操作: LEFT$, MID$, RIGHT$, LEN などの関数や、文字列用ガベージコレクション

結論として、m6502_basic.go はオリジナルの構造を理解するための学習用スケルトン、あるいは開発初期段階のコードと言えます。

ソースコードの量が、元のアセンブラーのコードに比べて少ないなと思ったら全部は移植されていませんでした。AIどころか、プログラム用エディタもない時代にすごいものを作ったと感動しました。

10
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?