diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c6f468..a5a067f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,6 +129,8 @@ jobs: run: | "$CLANG" --version python -m unittest discover -s tools/llvm-src32 -p 'test_*.py' -v + - name: Foundation shell (testbench, PCG screen) + run: python -m unittest discover -s foundation/tests/shell -v # sol コンパイラ / VM / LSP、WAX ツール、Foundation WAX ローダのホストテスト。 # sol の Foundation 実行テストは testbench を使う。 diff --git a/AGENTS.md b/AGENTS.md index 19bfdba..db0b1e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,7 @@ - sol VS Code 拡張(`tools/vscode-sol-syntax/` で実行): `npm run compile`、`npx vsce package --no-dependencies` - PCG scroll: `cargo test --test vdp_pcg_scroll` - Foundation WAX ローダ(ホスト): `python3 -m unittest discover -s foundation/tests/wax`。testbench での実行は `tools/llvm-src32/test_toolchain.py` の `test_foundation_wax_loader` +- Foundation シェル(testbench、PCG 画面の比較): `python3 -m unittest discover -s foundation/tests/shell`(`src32_testbench` と Clang が必要) - WASM SDK package builder: `python3 -m unittest discover -s sdk/wasm-expansion-card -p 'test_*.py' -v` - WASM starter/example: `cargo build --manifest-path templates/wasm-expansion-card/Cargo.toml --release --target wasm32-unknown-unknown` と同じ形式で example の manifest も build。 - sol ゲストの実行確認は、必要に応じて `tools/solc/solc.py compile` → `tools/asm/asm.py` → `src32_testbench` またはエミュレータの順に行います。 diff --git a/MEMORY.md b/MEMORY.md index b30b92e..faf11b3 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1058,3 +1058,12 @@ - Release (CD): .github/workflows/release.yml. Pushing a `v*` tag builds cpt32 / cpt32-tool / src32_testbench in release mode for linux-x86_64, windows-x86_64 and macos-arm64 (archive also carries README, licenses, config.example.yaml, assets/bios.rom) plus both VS Code .vsix files, and publishes them with softprops/action-gh-release (tags containing `-` become prereleases). PRs touching release.yml and workflow_dispatch run the builds without publishing. CI's `vscode-extensions` job also packages the .vsix on every PR. - `toolchain` and `solc` run with `if: !cancelled()` after `rust`, so a failing integration test does not hide them; they only need the uploaded testbench artifact. - dos_smoke hex tests can hang on CI runners (seen on dos_pipeline_hex_returns_to_shell_at_default_cycles_per_frame: keys for "EXIT" never reach the guest, CPU halted with IRQC pending=0x0001 enable=0x0008). connect_pec_input applies host key events on a separate `pec-input` thread, so the test races with the guest; under CPU contention an input edge can be lost. Not reproducible locally in 3 full runs. Same symptom as the known 800-cycle failure. + +## Foundation minimal shell on PCG (2026-10-22) +- foundation/shell/shell.c implements the parts of doc/spec_Foundation_shell.md that need no fs, IPC or proc_spawn: lexing/syntax §3–§4 (whole line checked before anything runs), variables §5.1, line editor §9.2, messages §11.1, builtins CLS/ECHO/SET/VER/MEM/PS/HELP/EXIT and bare CD. fs-dependent features report FND_E_UNSUPPORTED; command lookup reports `command not found`. foundation/README.md "シェル" lists what is provisional. +- Provisional console: no console service exists (needs phase 2 IPC), so foundation/lib/fnd/con_pcg.c is linked into the shell and drives PCG mode 1 (80x30, 8x8) and the PeC FIFO directly. The boot image (foundation/boot) grants VDP 0x80000000–0x8000FFFF, VRAM 0x10000000–0x10002FFF and PeC input to slot 4 via the new grants argument of fnd_proc_spawn_wax (§10.3 windows; VRAM/PCMRAM exclusive -> FND_E_BUSY). The spec says the shell has caps=0 and no MMIO: remove the grants once the console service exists. MMIO regions carry `mmio=1` so §5.4 pointer checks skip them and exit does not zero them. +- Line editing uses only ST_WRITE controls: console BS at column 0 goes to the end of the previous row, so redraws are "print tail, blanks, BS back". Keep that BS rule if the console is rewritten. +- `--protected` used to emit one WAX block per `.ORG` gap (alignment padding between globals), so any C program with several globals hit "protected WAX allows at most 7 blocks". driver.py protected_blocks now fills gaps between same-permission blocks with zeros (regression in test_protected_wax_output). Zero-initialized globals are stored in the image (shell ~220 KiB). +- Spawning the 220 KiB shell with interrupts off loses ticks, so tests stop at tick 3000 but need ~200+ frames; tests/shell uses 260 frames. +- src32_testbench gained `--type FRAME=TEXT` (HID usages, US layout, escapes \n \b \L \R \U \D \H \E \X \C \; sleeps 50 ms after sending because the PeC input thread applies events asynchronously) and `--dump-pcg` (visible 30 rows honoring SCREEN_MODE and SCROLL_Y). foundation/tests/shell compares whole screens: about 40 s on the release testbench, 4.5 min on debug (CI). +- The PCG font comes only from config.yaml `pcg.font`; without it the GUI shows no glyphs (VRAM text is still right). diff --git a/doc/foundation_os_plan.md b/doc/foundation_os_plan.md index a1fc503..8bcee43 100644 --- a/doc/foundation_os_plan.md +++ b/doc/foundation_os_plan.md @@ -265,7 +265,7 @@ libfnd とサービスのプロトコルは [API 仕様](spec_Foundation_API.md) | 3 プロセス | `proc_spawn`/`exit`/`wait`、所有権表、`FNDBOOT` の同梱モジュール | 起動・終了を繰り返しても RAM とハンドルが漏れない。WAX v3 §8 の領域ケースのうち v2 で可能なもの | | 4 ディスク | `blk_io`(転送先の検証)、disk サービス、DSFS 読み取りマウント | FDD 読み込み中も他スレッドが進む。不正バッファでは `E_ACCESS` になり RAM は変化しない | | 5 FS2 | 仕様、ホストツール、ゲストの読み取り → 書き込み | ホストの fsck とゲストの結果が一致する。書込中断後はDIRTYでmountを拒否し、fsckの検査・修復可能範囲を確認する | -| 6 シェル | libfnd、shell、外部コマンド、stage1 からのディスク起動 | ディスクから起動して `DIR`、`TYPE`、アプリ起動を確認する | +| 6 シェル | libfnd、shell、外部コマンド、stage1 からのディスク起動 | ディスクから起動して `DIR`、`TYPE`、アプリ起動を確認する(fs なしで動く部分を先に実装済み: [`foundation/shell`](../foundation/README.md#シェル)。コンソールはシェルにリンクした暫定版で、PCG に表示する) | | 7 拡張 | WAX v3/WAL(libfnd の WAL 化)、再配置可能な WAX、`/tmp`、拡張カードのドライバ | WAX v3 §8 の全ケース(ローダは実装済み: `foundation/kernel/wax.c`、`tests/wax`。v2 PROTECTED も同じ経路で読み込む。v3 を生成するツールチェーンと libfnd の WAL 化は未着手) | 各フェーズでは、テストに通った focused check と、実行していない確認を分けて報告します。 diff --git a/doc/spec_Foundation_API.md b/doc/spec_Foundation_API.md index 63c5b64..cc83109 100644 --- a/doc/spec_Foundation_API.md +++ b/doc/spec_Foundation_API.md @@ -1,6 +1,6 @@ # Foundation API 仕様 v1 -Revision 0.1、2026-10-22。Status: **ドラフト(主要な設計判断は確定、未実装)**。本書は Foundation OS のサービス間プロトコルと、アプリ向けライブラリ libfnd の API を定めます。対象は、ストリーム(ファイル、コンソール、デバイス)、fs サービスのセッションとパス操作、ディレクトリの列挙、コンソールの入出力、起動ブロックと標準ストリーム、プロセスの起動、名前照合、API のエラーコードです。カーネル ABI は [spec_Foundation.md](spec_Foundation.md)、ディスク形式と VFS の意味は [spec_FS2.md](spec_FS2.md)、シェルは [spec_Foundation_shell.md](spec_Foundation_shell.md) を参照してください。本書と [実装案](foundation_os_plan.md) が異なる場合は本書が優先します。 +Revision 0.1、2026-10-22。Status: **ドラフト(主要な設計判断は確定、一部実装)**。libfnd のうち起動、標準ストリーム、コンソールの一部、`fnd_strerror` を [`foundation/lib/fnd`](../foundation/README.md#シェル) に実装しています。IPC がまだないので、コンソールはプログラムにリンクする暫定実装です。 本書は Foundation OS のサービス間プロトコルと、アプリ向けライブラリ libfnd の API を定めます。対象は、ストリーム(ファイル、コンソール、デバイス)、fs サービスのセッションとパス操作、ディレクトリの列挙、コンソールの入出力、起動ブロックと標準ストリーム、プロセスの起動、名前照合、API のエラーコードです。カーネル ABI は [spec_Foundation.md](spec_Foundation.md)、ディスク形式と VFS の意味は [spec_FS2.md](spec_FS2.md)、シェルは [spec_Foundation_shell.md](spec_Foundation_shell.md) を参照してください。本書と [実装案](foundation_os_plan.md) が異なる場合は本書が優先します。 「〜しなければならない (MUST)」「〜してはならない (MUST NOT)」「〜してよい (MAY)」は規範的な要求を表します。 diff --git a/doc/spec_Foundation_shell.md b/doc/spec_Foundation_shell.md index e87b01a..a5e043b 100644 --- a/doc/spec_Foundation_shell.md +++ b/doc/spec_Foundation_shell.md @@ -1,6 +1,6 @@ # Foundation シェル仕様 v1 -Revision 0.1、2026-10-22。Status: **ドラフト(主要な設計判断は確定、未実装)**。本書は Foundation OS の標準シェル `/bin/shell.wax` の起動、構文、展開、コマンドの解決と実行、リダイレクト、組み込みコマンド、スクリプト、対話入力を定めます。カーネル ABI は [spec_Foundation.md](spec_Foundation.md)、ファイルシステムと VFS は [spec_FS2.md](spec_FS2.md)、背景は [実装案 §7](foundation_os_plan.md) を参照してください。本書と実装案が異なる場合は本書が優先します。 +Revision 0.1、2026-10-22。Status: **ドラフト(主要な設計判断は確定、一部実装)**。fs、`proc_spawn`、console サービスがなくても動く部分を [`foundation/shell`](../foundation/README.md#シェル) に実装しています(実装済みの範囲と暫定の扱いは README)。 本書は Foundation OS の標準シェル `/bin/shell.wax` の起動、構文、展開、コマンドの解決と実行、リダイレクト、組み込みコマンド、スクリプト、対話入力を定めます。カーネル ABI は [spec_Foundation.md](spec_Foundation.md)、ファイルシステムと VFS は [spec_FS2.md](spec_FS2.md)、背景は [実装案 §7](foundation_os_plan.md) を参照してください。本書と実装案が異なる場合は本書が優先します。 「〜しなければならない (MUST)」「〜してはならない (MUST NOT)」「〜してよい (MAY)」は規範的な要求を表します。 diff --git a/foundation/README.md b/foundation/README.md index 1932188..9f6bf25 100644 --- a/foundation/README.md +++ b/foundation/README.md @@ -9,7 +9,7 @@ CPT32 用の OS です。契約は [Foundation 仕様](../doc/spec_Foundation.md - **トラップ**(§3.4): IRQ、SYSCALL、U モードの例外を `kernel_trap` で振り分けます。カーネルは割り込み禁止のまま、カーネルスタック1本で動きます。U モードで fault が起きたら、そのプロセスを終了させます。終了コードは CAUSE を i32 として扱った値で、fault の EPC も記録します。idle で例外が起きた場合は `kernel_panic` になります。 - **スケジューラ**(§10.1): 優先度は DRIVER / SERVICE / APP / IDLE の4段で、同じ優先度の中は FIFO のラウンドロビンです。tick は PeC timer ch0 の 1000 Hz(IRQ4)、タイムスライスは 10 tick です。より高い優先度のスレッドが起きたら、カーネルからユーザーへ戻る時点で切り替えます。idle は S モードのスレッドで、`HALT` を繰り返します。 - **プロセスと保護**(§5): 7つのユーザースロットを使います。別プロセスへ切り替えるときは、§5.3 の順(全 FLAGS=0 → START/END → FLAGS)で8領域を書き換えます。終了時は全スレッドを DEAD にし、付与した RAM をゼロにします。ユーザーポインタは §5.4 の手順で検証します。 -- **システムコール**(§7, §9): `yield`、`sleep`(`FND_FOREVER` を含む)、`thread_exit`、`proc_exit`、`time_get`、`debug_write` を実装しています。ABI で定義済みでも未実装のものは `FND_E_UNSUPPORTED`、未定義の番号と context byte が 0 でない呼び出しは `FND_E_BAD_CALL` を返します。 +- **システムコール**(§7, §9): `sys_info`(sel 0 と 1)、`yield`、`sleep`(`FND_FOREVER` を含む)、`thread_exit`、`proc_exit`、`time_get`、`debug_write` を実装しています。ABI で定義済みでも未実装のものは `FND_E_UNSUPPORTED`、未定義の番号と context byte が 0 でない呼び出しは `FND_E_BAD_CALL` を返します。 `fnd_proc_create_builtin()` は、カーネルイメージにリンクしたユーザーコードからプロセスを作ります。WAX から作る方法は次の節を参照してください。このとき `[code_start, code_end)` が R/X、スロット上端の `ram_size` byte が R/W(スタックとデータ)になります。そのためユーザーコードでは、グローバル変数・文字列リテラル・範囲外の関数呼び出しを使えません(`examples/kernel-trap-c` と同じ制約)。システムコールは `` のマクロで呼ぶので、インライン展開されます。 @@ -22,22 +22,50 @@ CPT32 用の OS です。契約は [Foundation 仕様](../doc/spec_Foundation.md | `kernel/proc.c` | プロセス、スロット、保護領域、ポインタ検証、終了処理 | | `kernel/trap.c` | `kernel_trap` / `kernel_panic`、IRQ、システムコール | | `kernel/wax.c`, `wax.h` | WAX ローダの本体(検証、依存解決、WAL の配置、領域の計画、束縛) | +| `include/foundation/api.h`, `fnd.h`, `lib/fnd/` | libfnd([API 仕様 §11](../doc/spec_Foundation_API.md))のうち、今あるもの。コンソールは暫定実装 | +| `shell/` | シェル([シェル仕様](../doc/spec_Foundation_shell.md))の、いまのカーネルで動く部分 | +| `boot/` | シェルを起動する起動イメージと、そのビルドスクリプト | | `demo/` | 画面に各プロセスの状態を表示するデモ | | `tests/phase1/` | フェーズ1の受け入れテスト | | `tests/wax/` | WAX ローダのテスト(ホスト単体テストと testbench) | +| `tests/shell/` | シェルのテスト(testbench にキー入力を送り、PCG の画面を比べる) | ## WAX ローダ -`fnd_proc_spawn_wax(name, priority, image, size, libs, lib_count)` は、カーネルメモリ上の WAX を、その `base_addr` を含むユーザースロットに読み込んで起動します([仕様 §9.2・§9.5](../doc/spec_Foundation.md))。 +`fnd_proc_spawn_wax(name, priority, image, size, libs, lib_count, grants, grant_count)` は、カーネルメモリ上の WAX を、その `base_addr` を含むユーザースロットに読み込んで起動します([仕様 §9.2・§9.5](../doc/spec_Foundation.md))。 - 形式は PROTECTED の [WAX v2](../doc/spec_WAX.md) と [WAX/WAL v3](../doc/spec_WAX_v3.md) です。v3 では、依存する WAL を `libs` から選び(同じ ID と major の中で最大の minor)、スロット内に first fit で置き、import スロットに記述子(アドレスと origin)を書きます。 - 保護領域は region 0 がスタック、その後が全画像のブロックをアドレス順に並べたものです。隣接していて権限が同じブロックだけをまとめ、8 を超えると `FND_E_REGION_LIMIT` です。 +- `grants` は MMIO の付与(§9.2 の `grants`、§10.3)で、画像の後の region になります(§5.3 の手順 4)。§10.3 の窓に収まらないものは `FND_E_ACCESS`、R(1)と RW(3)以外の perm は `FND_E_BAD_ARG` です。VRAM と PCMRAM は 1 プロセスにしか付与できず、動いている別のプロセスが持っていれば `FND_E_BUSY` です(§10.3 は誤りのコードを定めていないので、カーネルのコードから選んだ)。付与した範囲は §5.4 のポインタ検証の対象外で、終了時にゼロにもしません。 - 起動は原子的です。検証、依存解決、配置、領域とシンボルの確認がすべて通るまで、メモリにもプロセス表にも書きません。 - 入口の状態は §9.2 のとおりです(`USP = user_stack_top`、`UGP = base_addr`、他のレジスタは 0、`IE = 1`、スタックはゼロクリア)。終了時は、付与した範囲をすべてゼロにします。 - `wax.c` はカーネルの状態にもデバイスにも触れない C11 なので、ホストの C コンパイラでもビルドできます。 `proc_spawn` システムコールは、ハンドル(`PROCESS`)が実装されるフェーズ2以降に、この関数の上に作ります。そのときは §9.2 のとおり、イメージを先に staging へ複製してから検証します(現在の関数は、渡されたイメージが呼び出し中に変わらないことを前提にしています)。ライブラリ一覧をどこから得るかは未決です(§9.5)。 +## シェル + +`shell/shell.c` は [シェル仕様](../doc/spec_Foundation_shell.md) のうち、いまのカーネル(IPC、ハンドル、fs がない)で動く部分です。PROTECTED WAX v2 としてスロット 4(`0x600000`)に置き、APP 優先度で動きます。 + +- **できること**:字句と構文(§3、§4。引用、`;`、`&&`、`||`、コメント、予約記号のエラー、構文エラーの行は何も実行しない)、変数(§5.1。`SET`、`$NAME`、`${NAME}`、`$?`、`$0`、`$#`、`$1`〜`$9`、既定の `PATH`)、行編集(§9.2。←→、Home、End、Backspace、Delete、16 行の履歴、Ctrl+C)、エラーメッセージ(§11.1)、組み込みコマンド `CLS`、`ECHO`、`SET`、`VER`、`MEM`、`PS`、`HELP`、`EXIT`、引数なしの `CD`。プロンプトは `/> ` です。 +- **まだできないこと**:fs サービスがないので、ファイルへのリダイレクト(`2>&1` は使える)、ワイルドカード、`CD path`、`DIR`、`TYPE`、`COPY`、`DEL`、`REN`、`MD`、`RD` は `FND_E_UNSUPPORTED`、外部コマンドとパスでの指定は `command not found`(`FND_E_NOT_FOUND`)になります。スクリプト、`-c`、`/etc/profile.fsh` もありません。`proc_spawn` がないので外部プログラムを起動できず、`thread_create` と `CON_BREAK_WAIT` がないので、Ctrl+C は行編集中の入力を捨てるだけです。 +- **暫定の部分**:console サービス(フェーズ 2 の IPC が必要)の代わりに、`lib/fnd/con_pcg.c` をシェルにリンクしています。これが PCG(SCREEN_MODE 1、80×30、8×8)と PeC のキーボードを MMIO で直接扱い、出力は [API 仕様 §8.1](../doc/spec_Foundation_API.md)、キーは §8.3 のキーコードに従います。キー入力は `irq_bind` がないので、FIFO を 4 tick ごとに見ます。`fnd_init` は起動ブロックを読めない(`proc_startup` が未実装)ので argc = 0 とし、STDIN、STDOUT、STDERR、CONSOLE の役割をすべてこのコンソール(ハンドル 1)にします。シェル仕様ではシェルは `caps = 0` で MMIO を持ちませんが、これを入れ替えるまでは、起動イメージが VDP の窓、PCG のテキスト面と属性面(VRAM `0x10000000–0x10002FFF`)、PeC の入力窓を付与します。 +- **起動イメージ**:`boot/main.c` はカーネルとシェルの画像(`boot/build.py` が生成する `shell_image.h`)をリンクしたものです。シェルが終了すると、次の 100 tick 境界で起動し直します(init の代わり)。 +- `VER`、`MEM`、`PS` は `sys_info` を使います。`ram_bytes` は §5.1 の前提の 16 MiB、`free_bytes` は動いているプロセスが占めていないユーザースロットの合計、`parent` と `boot_drive` は 0 です(`proc_spawn` と stage1 がまだない)。 + +```sh +python foundation/boot/build.py target/foundation-boot +cargo run --release --bin cpt32 -- target/foundation-boot/foundation.wax +``` + +PCG のフォントはエミュレーターの設定(`config.yaml` の `pcg.font`、例 `assets/UNSCII.F08`)で読み込みます。設定がないと文字が表示されません。 + +`tests/shell/test_shell.py` は、`--stop-ticks 3000` でビルドした起動イメージを `src32_testbench` で 260 frame 動かします。`--type FRAME=TEXT` でその frame にキーを送り(`\n` Enter、`\b` Backspace、`\L` `\R` `\U` `\D` 矢印、`\H` Home、`\E` End、`\X` Delete、`\C` Ctrl+C、`\\` バックスラッシュ)、`--dump-pcg` で最後の画面(SCROLL_Y を反映した 30 行)を出力させて比べます。GUI は Xvfb の下では黒い画面になるので、画面の確認はこの方法で行います。 + +```sh +python -m unittest discover -s foundation/tests/shell -v +``` + ## ビルドと実行 カーネルは §5.1 の配置(イメージ `0x10000`、カーネルスタック頂上 `0x100000`)でビルドします。各 `user.c` では `user_code_begin` を最初の関数、`user_code_end` を最後の関数にします。 @@ -113,5 +141,6 @@ src32_testbench /absolute/path/target/foundation-wax/test.wax 42 --cpu pipeline - `src32_testbench` には UART がありません。そのため、そこで `debug_write` に 1 byte 以上を渡したり `kernel_panic` が出力したりすると、カーネル内でバスエラーになります。エミュレーター本体では UART に出力されます。 - RAM 容量の取得方法(§4.1)はまだ決めていません。スロットは既定の 16 MiB を前提にしています。 - 1プロセス1スレッドです。`thread_create`、IPC、ハンドル、`proc_spawn` システムコール、`mem_grow`、`irq_bind` などはフェーズ2以降で実装します。 +- `--protected` の C プログラムでは、アセンブラが `.ORG` で分けたブロックのうち、同じ権限で並ぶものの隙間(整列の埋め草)をゼロで埋めて 1 つにします(PROTECTED WAX は 7 ブロックまで)。ゼロ初期化のグローバルもイメージに含まれるので、シェルのイメージは約 220 KiB です。 - WAX の読み込み中は割り込み禁止です。`tests/wax` の起動 11 回(うち 5 回は失敗)は、-O0 の Scalar で約 16M サイクル(1 回あたり約 30 tick 分)かかります。-O2 では約 10M サイクルです。 - timer の ACK から IRQC の W1C までの間に次の tick が来ると、IRQC の edge が失われます。カーネルは W1C の後に timer の pending をもう一度確認して、取りこぼしを防いでいます。 diff --git a/foundation/boot/build.py b/foundation/boot/build.py new file mode 100644 index 0000000..e74ac69 --- /dev/null +++ b/foundation/boot/build.py @@ -0,0 +1,76 @@ +"""Builds the Foundation boot image with the shell into OUT/foundation.wax. + +The shell is a PROTECTED WAX v2 for slot 4 (doc/spec_Foundation_shell.md §1), +built with tools/llvm-src32 --protected. Until FNDBOOT exists, its bytes are +linked into the kernel image through OUT/shell_image.h (boot/main.c). + + python foundation/boot/build.py OUT [--clang PATH] [-O0|-O1|-O2] [--stop-ticks N] + +--stop-ticks makes the kernel stop with R1 = 42 at that tick (tests/shell). +""" +import argparse +from pathlib import Path +import subprocess +import sys + +HERE = Path(__file__).resolve().parent +FOUNDATION = HERE.parent +ROOT = FOUNDATION.parent +DRIVER = ROOT / "tools/llvm-src32/driver.py" + +# Slot 4 (doc/spec_Foundation.md §5.2) and the shell's stack. +SHELL_ORIGIN = 0x00600000 +SHELL_STACK_TOP = 0x00800000 +SHELL_STACK = 0x4000 + +INCLUDES = ["-I", str(ROOT / "sdk/cpt32/include"), "-I", str(FOUNDATION / "include"), + "-I", str(FOUNDATION / "lib/fnd")] +LIBFND = [FOUNDATION / "lib/fnd" / name for name in ("start.c", "con_pcg.c", "strerror.c")] +KERNEL = [FOUNDATION / "kernel" / name for name in ("sched.c", "proc.c", "trap.c", "wax.c")] + + +def driver(sources, output, args, *extra): + command = [sys.executable, str(DRIVER), *map(str, sources), *INCLUDES, args.optimization, + *extra, "-o", str(output)] + if args.clang: + command[2:2] = ["--clang", args.clang] + subprocess.run(command, check=True) + return output + + +def embed(name, data): + """Big-endian words: the same bytes in SRC32 memory, 4-byte aligned.""" + padded = data + bytes(-len(data) % 4) + words = ",".join("0x%Xu" % int.from_bytes(padded[i:i + 4], "big") for i in range(0, len(padded), 4)) + return (f"#define img_{name}_size {len(data)}u\n" + f"static const uint32_t img_{name}[{len(padded) // 4}] = {{{words}}};\n") + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("out", type=Path) + parser.add_argument("--clang") + parser.add_argument("--stop-ticks", type=int) + level = parser.add_mutually_exclusive_group() + for n in range(3): + level.add_argument(f"-O{n}", dest="optimization", action="store_const", const=f"-O{n}") + parser.set_defaults(optimization="-O1") + args = parser.parse_args(argv) + args.out.mkdir(parents=True, exist_ok=True) + + shell = driver([FOUNDATION / "shell/shell.c", *LIBFND], args.out / "shell.wax", args, "--protected", + "--origin", hex(SHELL_ORIGIN), "--stack-top", hex(SHELL_STACK_TOP), + "--user-stack-size", hex(SHELL_STACK)) + header = ["/* Generated by foundation/boot/build.py. */", "#include ", + embed("shell", shell.read_bytes())] + (args.out / "shell_image.h").write_text("\n".join(header), encoding="utf-8") + extra = ["-I", str(args.out), "--origin", "0x10000", "--stack-top", "0x100000"] + if args.stop_ticks is not None: + extra += ["-D", f"FOUNDATION_BOOT_STOP_TICKS={args.stop_ticks}u"] + image = driver([HERE / "main.c", *KERNEL], args.out / "foundation.wax", args, *extra) + print(f"Wrote {image}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/foundation/boot/main.c b/foundation/boot/main.c new file mode 100644 index 0000000..a48396c --- /dev/null +++ b/foundation/boot/main.c @@ -0,0 +1,51 @@ +/* Foundation boot image: the kernel starts the shell (/bin/shell.wax, + * doc/spec_Foundation_shell.md) in slot 4 at APP priority. + * + * Provisional until init, the console service and FNDBOOT exist: the shell + * image is linked into the kernel image (shell_image.h from build.py), and + * the shell drives the console itself (foundation/lib/fnd/con_pcg.c), so it + * gets MMIO grants for the PCG and the PeC input. When the shell exits, the + * kernel starts it again, as init would. */ +#include + +#include "con_pcg.h" +#include "shell_image.h" + +#define RESPAWN_TICKS 100u +#define RW (CPT32_PERM_R | CPT32_PERM_W) + +static const struct fnd_grant console_grants[] = { + {CON_PCG_VDP_START, CON_PCG_VDP_END, RW}, + {CON_PCG_TEXT_START, CON_PCG_TEXT_END, RW}, + {CON_PCG_INPUT_START, CON_PCG_INPUT_END, RW}, +}; +#define GRANTS (sizeof console_grants / sizeof console_grants[0]) + +static int32_t shell_pid; + +static int32_t spawn_shell(void) { + return fnd_proc_spawn_wax("shell", FND_PRIO_APP, img_shell, img_shell_size, 0, 0, console_grants, + GRANTS); +} + +static uint32_t on_tick(uint32_t ticks) { + if (ticks % RESPAWN_TICKS == 0) { + struct fnd_proc *proc = fnd_proc_get((uint32_t)shell_pid); + if (!proc || proc->state != FND_PROC_LIVE) + shell_pid = spawn_shell(); + } +#ifdef FOUNDATION_BOOT_STOP_TICKS + /* tests/shell: stop the machine so src32_testbench can read the screen */ + if (ticks == FOUNDATION_BOOT_STOP_TICKS) + return 42u; +#endif + return 0; +} + +int main(void) { + shell_pid = spawn_shell(); + if (shell_pid < 0) + return 1; + fnd_set_tick_hook(on_tick); + fnd_kernel_run(); +} diff --git a/foundation/include/foundation/api.h b/foundation/include/foundation/api.h new file mode 100644 index 0000000..6ec7998 --- /dev/null +++ b/foundation/include/foundation/api.h @@ -0,0 +1,53 @@ +#ifndef FOUNDATION_API_H +#define FOUNDATION_API_H +#include + +/* Numbers of the Foundation API (doc/spec_Foundation_API.md). Only the parts + * that libfnd implements so far are listed. */ + +/* §3: API error codes, after the kernel's -1..-63. */ +#define FND_E_NOT_DIR (-64) +#define FND_E_IS_DIR (-65) +#define FND_E_NOT_EMPTY (-66) +#define FND_E_BAD_PATH (-67) +#define FND_E_NAME_TOO_LONG (-68) +#define FND_E_NO_SPACE (-69) +#define FND_E_CROSS_DEVICE (-70) +#define FND_E_INTERRUPTED (-71) +#define FND_E_NEEDS_FSCK (-72) + +/* §11.1: the end of a listing or of the input. */ +#define FND_END 1 + +/* §9.3 of the kernel ABI: roles of the startup block. */ +#define FND_ROLE_STDIN 1u +#define FND_ROLE_STDOUT 2u +#define FND_ROLE_STDERR 3u +#define FND_ROLE_FS 4u +#define FND_ROLE_CONSOLE 5u + +/* §8.3: console key codes of CON_READ_KEY and modifier bits. */ +#define FND_KEY_ENTER 1u +#define FND_KEY_BACKSPACE 2u +#define FND_KEY_DELETE 3u +#define FND_KEY_LEFT 4u +#define FND_KEY_RIGHT 5u +#define FND_KEY_UP 6u +#define FND_KEY_DOWN 7u +#define FND_KEY_HOME 8u +#define FND_KEY_END 9u +#define FND_KEY_TAB 10u +#define FND_KEY_ESC 11u +#define FND_KEY_CHAR 0x100u + +#define FND_MOD_SHIFT 1u +#define FND_MOD_CTRL 2u +#define FND_MOD_ALT 4u + +/* Kernel ABI §9 sys_info. */ +#define FND_INFO_SYSTEM 0u +#define FND_INFO_PROCESSES 1u +#define FND_SYSINFO_BYTES 32u +#define FND_PROCINFO_BYTES 48u + +#endif diff --git a/foundation/include/foundation/fnd.h b/foundation/include/foundation/fnd.h new file mode 100644 index 0000000..768b196 --- /dev/null +++ b/foundation/include/foundation/fnd.h @@ -0,0 +1,50 @@ +#ifndef FOUNDATION_FND_H +#define FOUNDATION_FND_H +#include + +#include + +/* libfnd (doc/spec_Foundation_API.md §11), the part that exists so far. + * + * Provisional: the kernel has no IPC and no handles yet, so there is no + * console service. Until there is, the console is lib/fnd/con_pcg.c, linked + * into the program, which drives the PCG and the PeC input itself through + * MMIO grants. fnd_init then gives every console role (STDIN, STDOUT, + * STDERR, CONSOLE) the handle FND_LOCAL_CONSOLE, as init would pass one + * console stream for all four (§12); there is no FS role. */ + +typedef int32_t fnd_status; +typedef uint32_t fnd_handle; + +#define FND_LOCAL_CONSOLE 1u + +typedef struct { + uint32_t code; /* FND_KEY_* */ + uint32_t ch; /* Unicode scalar for FND_KEY_CHAR, else 0 */ + uint32_t mods; /* FND_MOD_* */ +} fnd_key_t; + +/* §11.2 */ +fnd_status fnd_init(void); +uint32_t fnd_argc(void); +const char *fnd_argv(uint32_t i); +fnd_handle fnd_std_in(void); +fnd_handle fnd_std_out(void); +fnd_handle fnd_std_err(void); +fnd_handle fnd_console(void); +fnd_handle fnd_fs(void); +void fnd_exit(int32_t code) __attribute__((noreturn)); + +/* §11.3 */ +fnd_status fnd_write(fnd_handle h, const void *buf, uint32_t len, uint32_t *put); + +/* §11.5 */ +fnd_status fnd_con_write(const char *text); +fnd_status fnd_con_getkey(fnd_key_t *key); +fnd_status fnd_con_clear(void); +fnd_status fnd_con_size(uint32_t *cols, uint32_t *rows); + +/* §11.9 */ +const char *fnd_strerror(fnd_status code); + +#endif diff --git a/foundation/include/foundation/kernel.h b/foundation/include/foundation/kernel.h index f90a53b..baeef3e 100644 --- a/foundation/include/foundation/kernel.h +++ b/foundation/include/foundation/kernel.h @@ -25,6 +25,7 @@ enum fnd_proc_state { FND_PROC_FREE, FND_PROC_LIVE, FND_PROC_EXITED }; struct fnd_region { uint32_t start, end, flags; uint32_t owned; /* zeroed and released when the process ends */ + uint32_t mmio; /* an MMIO grant (§10.3): never a RAM region for §5.4 */ }; struct fnd_proc { @@ -56,15 +57,26 @@ struct fnd_image { }; #define FND_MAX_LIBRARIES 16u +/* An MMIO grant of fnd_spawn (§9.2, §10.3): [start, end) with perm R (1) + * or RW (3). */ +struct fnd_grant { + uint32_t start, end, perm; +}; +#define FND_MAX_GRANTS 8u + /* Loads a PROTECTED WAX into the user slot that contains its base_addr and * starts it as §9.2 describes: v2, or v3 with the WAL graph it needs, chosen * from libs (doc/spec_WAX_v3.md §4.1, §7). The image and the libraries must * stay unchanged during the call; the kernel does not copy them first. - * Atomic: on failure nothing is allocated or written. Returns the pid (>= 1) - * or FND_E_FORMAT, FND_E_UNSUPPORTED, FND_E_DEPENDENCY, FND_E_REGION_LIMIT, - * FND_E_ADDRESS_CONFLICT (slot in use), FND_E_LIMIT or FND_E_BAD_ARG. */ + * grants become the last regions (§5.3 step 4); a grant outside the §10.3 + * windows is FND_E_ACCESS, and VRAM or PCMRAM already granted to a live + * process is FND_E_BUSY. Atomic: on failure nothing is allocated or written. + * Returns the pid (>= 1) or FND_E_FORMAT, FND_E_UNSUPPORTED, + * FND_E_DEPENDENCY, FND_E_REGION_LIMIT, FND_E_ADDRESS_CONFLICT (slot in + * use), FND_E_LIMIT, FND_E_ACCESS, FND_E_BUSY or FND_E_BAD_ARG. */ int32_t fnd_proc_spawn_wax(const char *name, uint32_t priority, const void *image, uint32_t size, - const struct fnd_image *libs, uint32_t lib_count); + const struct fnd_image *libs, uint32_t lib_count, + const struct fnd_grant *grants, uint32_t grant_count); /* NULL for an unknown pid. Exited processes keep their record. */ struct fnd_proc *fnd_proc_get(uint32_t pid); diff --git a/foundation/include/foundation/sys.h b/foundation/include/foundation/sys.h index 4d5c494..56ac738 100644 --- a/foundation/include/foundation/sys.h +++ b/foundation/include/foundation/sys.h @@ -6,6 +6,8 @@ /* User-mode system call wrappers (doc/spec_Foundation.md §7, §12). They are * macros over the cpt32_syscall intrinsic, so they expand inline and also * work in code that may not call outside its granted range. */ +#define fnd_sys_info(selector, buffer, cap) \ + ((int32_t)CPT32_SYSCALL3(FND_SYS_INFO, (selector), (uint32_t)(uintptr_t)(buffer), (cap))) #define fnd_sys_yield() ((int32_t)CPT32_SYSCALL0(FND_SYS_YIELD)) #define fnd_sys_sleep(ticks) ((int32_t)CPT32_SYSCALL1(FND_SYS_SLEEP, (ticks))) #define fnd_sys_thread_exit(code) ((void)CPT32_SYSCALL1(FND_SYS_THREAD_EXIT, (uint32_t)(code))) diff --git a/foundation/kernel/internal.h b/foundation/kernel/internal.h index 6cc15e5..ff09ab5 100644 --- a/foundation/kernel/internal.h +++ b/foundation/kernel/internal.h @@ -28,12 +28,14 @@ void sched_tick(void); void sched_yield(void); void sched_sleep(uint32_t ticks); void sched_kill_proc(struct fnd_proc *proc); +int sched_proc_runnable(const struct fnd_proc *proc); cpt32_ctx_frame *sched_next(void); /* proc.c */ void proc_activate(struct fnd_proc *proc); int proc_user_range_ok(const struct fnd_proc *proc, uint32_t addr, uint32_t len, uint32_t perm); void proc_exit(struct fnd_proc *proc, int32_t code, uint32_t fault_epc); +int32_t proc_sys_info(const struct fnd_proc *caller, uint32_t sel, uint32_t buf, uint32_t cap); /* MMIO (doc/spec_PeC.md, doc/spec_IRQC.md). */ #define FND_REG32(address) (*(volatile uint32_t *)(uintptr_t)(address)) diff --git a/foundation/kernel/proc.c b/foundation/kernel/proc.c index 56e407c..a68c4a4 100644 --- a/foundation/kernel/proc.c +++ b/foundation/kernel/proc.c @@ -68,7 +68,7 @@ int proc_user_range_ok(const struct fnd_proc *proc, uint32_t addr, uint32_t len, uint32_t covered = 0; for (uint32_t i = 0; i < proc->region_count; ++i) { const struct fnd_region *r = &proc->regions[i]; - if ((r->flags & perm) == perm && r->start <= addr && addr < r->end) { + if (!r->mmio && (r->flags & perm) == perm && r->start <= addr && addr < r->end) { covered = r->end - addr; break; } @@ -187,8 +187,8 @@ int32_t fnd_proc_create_builtin(const char *name, uint32_t slot, uint32_t priori zero_range(ram, end); /* Region 0: stack and data at the top of the slot (RW). Region 1: the * code (RX). */ - proc->regions[0] = (struct fnd_region){ram, end, CPT32_PERM_R | CPT32_PERM_W, 1}; - proc->regions[1] = (struct fnd_region){code_start, code_end, CPT32_PERM_R | CPT32_PERM_X, 0}; + proc->regions[0] = (struct fnd_region){ram, end, CPT32_PERM_R | CPT32_PERM_W, 1, 0}; + proc->regions[1] = (struct fnd_region){code_start, code_end, CPT32_PERM_R | CPT32_PERM_X, 0, 0}; proc->region_count = 2; uint32_t sp = end - 8u; *(volatile uint32_t *)(uintptr_t)sp = arg; @@ -218,15 +218,73 @@ static int32_t wax_status(enum wax_error error) { } } +/* MMIO windows that may be granted (§10.3). VRAM and PCMRAM go to one + * process at a time. */ +static const struct { + uint32_t start, end, exclusive; +} grant_windows[] = { + {0x80000000u, 0x80010000u, 0}, /* VDP */ + {0x80010000u, 0x80020000u, 0}, /* SGC */ + {0x80020000u, 0x80030000u, 0}, /* SGU */ + {0x80040000u, 0x80040030u, 0}, /* PeC UART and RNG */ + {0x80040030u, 0x80040180u, 0}, /* PeC input */ + {0x10000000u, 0x18000000u, 1}, /* VRAM */ + {0x18000000u, 0x18100000u, 1}, /* PCMRAM */ +}; +#define GRANT_WINDOWS (sizeof grant_windows / sizeof grant_windows[0]) + +/* Index of the window that holds the whole grant, or -1. */ +static int grant_window(const struct fnd_grant *g) { + for (unsigned i = 0; i < GRANT_WINDOWS; ++i) { + if (grant_windows[i].start <= g->start && g->end <= grant_windows[i].end) + return (int)i; + } + return -1; +} + +/* A live process already holds part of an exclusive window. */ +static int window_taken(unsigned window) { + for (uint32_t p = 0; p < FND_MAX_PROCS; ++p) { + if (procs[p].state != FND_PROC_LIVE) + continue; + for (uint32_t i = 0; i < procs[p].region_count; ++i) { + const struct fnd_region *r = &procs[p].regions[i]; + if (r->mmio && r->start < grant_windows[window].end && grant_windows[window].start < r->end) + return 1; + } + } + return 0; +} + +static int32_t check_grants(const struct fnd_grant *grants, uint32_t count) { + if (count > FND_MAX_GRANTS || (count && !grants)) + return FND_E_BAD_ARG; + for (uint32_t i = 0; i < count; ++i) { + const struct fnd_grant *g = &grants[i]; + if (g->start >= g->end || (g->perm != CPT32_PERM_R && g->perm != (CPT32_PERM_R | CPT32_PERM_W))) + return FND_E_BAD_ARG; + int window = grant_window(g); + if (window < 0) + return FND_E_ACCESS; + if (grant_windows[window].exclusive && window_taken((unsigned)window)) + return FND_E_BUSY; + } + return FND_OK; +} + /* One launch at a time: the kernel runs with interrupts off. */ static struct wax_launch launch; int32_t fnd_proc_spawn_wax(const char *name, uint32_t priority, const void *image, uint32_t size, - const struct fnd_image *libs, uint32_t lib_count) { + const struct fnd_image *libs, uint32_t lib_count, + const struct fnd_grant *grants, uint32_t grant_count) { struct wax_library table[FND_MAX_LIBRARIES]; uint32_t base, slot = 0; if (priority > FND_PRIO_APP || lib_count > FND_MAX_LIBRARIES || (lib_count && !libs)) return FND_E_BAD_ARG; + int32_t status = check_grants(grants, grant_count); + if (status != FND_OK) + return status; enum wax_error error = wax_base_addr(image, size, &base); if (error != WAX_OK) return wax_status(error); @@ -234,7 +292,6 @@ int32_t fnd_proc_spawn_wax(const char *name, uint32_t priority, const void *imag ++slot; if (slot == FND_SLOTS) return FND_E_FORMAT; /* §5.2: the image must lie in one user slot */ - int32_t status; struct fnd_proc *proc = claim_record(slot, &status); if (!proc) return status; @@ -246,6 +303,8 @@ int32_t fnd_proc_spawn_wax(const char *name, uint32_t priority, const void *imag slot_bounds[slot + 1u]}; if ((error = wax_plan(&launch, image, size, table, lib_count, &target)) != WAX_OK) return wax_status(error); + if (launch.region_count + grant_count > FND_REGIONS) + return FND_E_REGION_LIMIT; /* §5.3: the grants come after the image */ struct fnd_thread *thread = sched_alloc_thread(); if (!thread) return FND_E_LIMIT; @@ -254,8 +313,85 @@ int32_t fnd_proc_spawn_wax(const char *name, uint32_t priority, const void *imag wax_load(&launch, 0); for (uint32_t i = 0; i < launch.region_count; ++i) { proc->regions[i] = (struct fnd_region){launch.region[i].start, launch.region[i].end, - launch.region[i].flags, 1}; + launch.region[i].flags, 1, 0}; + } + for (uint32_t i = 0; i < grant_count; ++i) { + proc->regions[launch.region_count + i] = + (struct fnd_region){grants[i].start, grants[i].end, grants[i].perm, 0, 1}; } - proc->region_count = launch.region_count; + proc->region_count = launch.region_count + grant_count; return start_proc(proc, thread, name, slot, priority, launch.entry, launch.stack_top, base); } + +/* sys_info (§9): sel 0 is fnd_sysinfo, sel 1 the process list. Copies up to + * cap bytes to buf and returns the size of the whole answer. */ +#define SYSINFO_BYTES 32u +#define PROCINFO_BYTES 48u +#define RAM_BYTES 0x01000000u /* §5.1: v1 assumes the default 16 MiB */ + +static uint32_t proc_mem_bytes(const struct fnd_proc *proc) { + uint32_t bytes = 0; + for (uint32_t i = 0; i < proc->region_count; ++i) { + if (proc->regions[i].owned) + bytes += proc->regions[i].end - proc->regions[i].start; + } + return bytes; +} + +static void put_word(uint32_t buf, uint32_t cap, uint32_t offset, uint32_t value) { + if (offset + 4u <= cap) + *(volatile uint32_t *)(uintptr_t)(buf + offset) = value; +} + +int32_t proc_sys_info(const struct fnd_proc *caller, uint32_t sel, uint32_t buf, uint32_t cap) { + uint32_t live = 0, used = 0, records = 0; + for (uint32_t i = 0; i < FND_MAX_PROCS; ++i) { + if (procs[i].state == FND_PROC_LIVE) { + ++live; + used += slot_bounds[procs[i].slot + 1u] - slot_bounds[procs[i].slot]; + } + if (procs[i].state != FND_PROC_FREE) + ++records; + } + uint32_t size; + if (sel == 0) + size = SYSINFO_BYTES; + else if (sel == 1) + size = records * PROCINFO_BYTES; + else + return FND_E_BAD_ARG; + if (cap > size) + cap = size; + if (cap && (buf & 3u)) + return FND_E_BAD_ARG; + if (!proc_user_range_ok(caller, buf, cap, CPT32_PERM_W)) + return FND_E_ACCESS; + if (sel == 0) { + uint32_t user = slot_bounds[FND_SLOTS] - slot_bounds[0]; + uint32_t info[8] = {FND_ABI_MAJOR, FND_ABI_MINOR, FND_TICK_HZ, RAM_BYTES, user - used, live, + 0 /* boot_drive: no stage1 yet */, 0}; + for (uint32_t w = 0; w < 8u; ++w) + put_word(buf, cap, 4u * w, info[w]); + return (int32_t)size; + } + uint32_t offset = 0; + for (uint32_t i = 0; i < FND_MAX_PROCS; ++i) { + const struct fnd_proc *p = &procs[i]; + if (p->state == FND_PROC_FREE) + continue; + uint32_t state = p->state == FND_PROC_EXITED ? 2u : sched_proc_runnable(p) ? 0u : 1u; + uint32_t info[8] = {p->pid, 0 /* parent: no proc_spawn yet */, state, p->priority, + (uint32_t)p->exit_code, p->state == FND_PROC_LIVE ? proc_mem_bytes(p) : 0, + p->threads, p->fault_epc}; + for (uint32_t w = 0; w < 8u; ++w) + put_word(buf, cap, offset + 4u * w, info[w]); + for (uint32_t w = 0; w < 4u; ++w) { + uint32_t word = 0; + for (uint32_t b = 0; b < 4u; ++b) + word = word << 8 | (uint8_t)p->name[4u * w + b]; + put_word(buf, cap, offset + 32u + 4u * w, word); + } + offset += PROCINFO_BYTES; + } + return (int32_t)size; +} diff --git a/foundation/kernel/sched.c b/foundation/kernel/sched.c index b9e8337..91f4357 100644 --- a/foundation/kernel/sched.c +++ b/foundation/kernel/sched.c @@ -174,6 +174,15 @@ void sched_kill_proc(struct fnd_proc *proc) { } } +/* Some thread of proc is ready or running (sys_info state 0). */ +int sched_proc_runnable(const struct fnd_proc *proc) { + for (unsigned i = 0; i < FND_MAX_THREADS; ++i) { + if (threads[i].proc == proc && (threads[i].state == T_READY || threads[i].state == T_RUNNING)) + return 1; + } + return 0; +} + /* Chooses the thread to resume after a trap and returns its frame. */ cpt32_ctx_frame *sched_next(void) { struct fnd_thread *current = fnd_current; diff --git a/foundation/kernel/trap.c b/foundation/kernel/trap.c index 8c55ea9..e7b7ff8 100644 --- a/foundation/kernel/trap.c +++ b/foundation/kernel/trap.c @@ -100,7 +100,7 @@ static int32_t time_get(uint32_t selector) { static void handle_syscall(cpt32_ctx_frame *frame, uint32_t cause) { struct fnd_thread *thread = fnd_current; struct fnd_proc *proc = thread->proc; - uint32_t a1 = frame->r[1], a2 = frame->r[2]; + uint32_t a1 = frame->r[1], a2 = frame->r[2], a3 = frame->r[3]; int32_t result; if (CAUSE_SYSCALL_CONTEXT(cause) != 0) { frame->r[1] = (uint32_t)FND_E_BAD_CALL; @@ -125,7 +125,10 @@ static void handle_syscall(cpt32_ctx_frame *frame, uint32_t cause) { case FND_SYS_DEBUG_WRITE: result = debug_write(proc, a1, a2); break; - case FND_SYS_INFO: case FND_SYS_IPC_CALL: case FND_SYS_IPC_RECV: + case FND_SYS_INFO: + result = proc_sys_info(proc, a1, a2, a3); + break; + case FND_SYS_IPC_CALL: case FND_SYS_IPC_RECV: case FND_SYS_IPC_REPLY: case FND_SYS_IPC_REPLY_RECV: case FND_SYS_PORT_CREATE: case FND_SYS_CHAN_CREATE: case FND_SYS_HANDLE_CLOSE: case FND_SYS_HANDLE_DUP: case FND_SYS_THREAD_CREATE: case FND_SYS_WAIT: case FND_SYS_PROC_SPAWN: diff --git a/foundation/lib/fnd/con_pcg.c b/foundation/lib/fnd/con_pcg.c new file mode 100644 index 0000000..9ff52b4 --- /dev/null +++ b/foundation/lib/fnd/con_pcg.c @@ -0,0 +1,281 @@ +/* Provisional console on the PCG (doc/spec_VDP.md) and the PeC keyboard + * (doc/spec_PeC.md), see con_pcg.h. The screen is SCREEN_MODE 1, 80x30 + * cells of 8x8. Scrolling moves SCROLL_Y by one cell row and clears only the + * ring row that comes back at the bottom; the cursor keeps its screen row + * apart from the physical ring row. */ +#include + +#include "con_pcg.h" + +#define MMIO8(address) (*(volatile uint8_t *)(uintptr_t)(address)) +#define MMIO32(address) (*(volatile uint32_t *)(uintptr_t)(address)) + +#define VDP_DISPLAY_MODE 0x80000001u +#define PCG_DISABLE 0x8000F000u +#define PCG_SCREEN_MODE 0x8000F003u +#define PCG_CURSOR_X 0x8000F005u +#define PCG_CURSOR_Y 0x8000F006u +#define PCG_CURSOR_ENABLE 0x8000F007u +#define PCG_CURSOR_BLINK 0x8000F009u +#define PCG_SCROLL_X_LO 0x8000F00Cu +#define PCG_SCROLL_X_HI 0x8000F00Du +#define PCG_SCROLL_Y_LO 0x8000F00Eu +#define PCG_SCROLL_Y_HI 0x8000F00Fu +#define PCG_FG 0x1000u +#define PCG_BG 0x2000u +#define CELL_HEIGHT 8u + +#define PEC_EVENT_DATA 0x80040030u +#define PEC_EVENT_STATUS 0x80040034u +#define PEC_EVENT_POP 0x80040038u +#define POLL_TICKS 4u + +#define COLOR_FG 0x3Fu +#define COLOR_BG 0x00u +#define SUBSTITUTE '?' + +static uint32_t col, row, top; /* cursor cell; ring row shown at screen row 0 */ + +static uint32_t ring_row(uint32_t screen_row) { return (top + screen_row) % CON_PCG_ROWS; } + +static void put_cell(uint32_t x, uint32_t y, uint32_t ch) { + uint32_t cell = CON_PCG_TEXT_START + ring_row(y) * CON_PCG_COLS + x; + MMIO8(cell) = (uint8_t)ch; + MMIO8(cell + PCG_FG) = COLOR_FG; + MMIO8(cell + PCG_BG) = COLOR_BG; +} + +static void clear_row(uint32_t y) { + for (uint32_t x = 0; x < CON_PCG_COLS; ++x) + put_cell(x, y, ' '); +} + +/* SCROLL_Y is a 16-bit pixel offset written low byte first. */ +static void set_scroll(void) { + uint32_t y = top * CELL_HEIGHT; + MMIO8(PCG_SCROLL_Y_LO) = (uint8_t)y; + MMIO8(PCG_SCROLL_Y_HI) = (uint8_t)(y >> 8); +} + +static void show_cursor(void) { + MMIO8(PCG_CURSOR_X) = (uint8_t)col; + MMIO8(PCG_CURSOR_Y) = (uint8_t)ring_row(row); +} + +static void newline(void) { + col = 0; + if (row + 1u < CON_PCG_ROWS) { + ++row; + return; + } + top = (top + 1u) % CON_PCG_ROWS; + set_scroll(); + clear_row(CON_PCG_ROWS - 1u); +} + +static void advance(void) { + if (++col == CON_PCG_COLS) + newline(); +} + +/* §8.1: LF, CR, BS and TAB are controls; other controls are not shown. The + * PCG font has one byte per cell, so scalars above ASCII show SUBSTITUTE. */ +static void put_scalar(uint32_t cp) { + switch (cp) { + case '\n': + newline(); + return; + case '\r': + col = 0; + return; + case '\b': + if (col) { + --col; + } else if (row) { + --row; + col = CON_PCG_COLS - 1u; + } + return; + case '\t': + do { + put_cell(col, row, ' '); + advance(); + } while (col % 8u); + return; + default: + break; + } + if (cp < 0x20u || cp == 0x7Fu || (cp >= 0x80u && cp < 0xA0u)) + return; + put_cell(col, row, cp < 0x7Fu ? cp : SUBSTITUTE); + advance(); +} + +void con_pcg_write(const uint8_t *text, uint32_t len) { + uint32_t i = 0; + while (i < len) { + uint32_t b = text[i], cp = SUBSTITUTE, n = 1, need = 0; + if (b < 0x80u) { + cp = b; + } else if ((b & 0xE0u) == 0xC0u) { + need = 1, cp = b & 0x1Fu; + } else if ((b & 0xF0u) == 0xE0u) { + need = 2, cp = b & 0x0Fu; + } else if ((b & 0xF8u) == 0xF0u) { + need = 3, cp = b & 0x07u; + } + for (uint32_t k = 0; k < need; ++k) { + if (i + n >= len || (text[i + n] & 0xC0u) != 0x80u) { + cp = SUBSTITUTE; /* malformed: one substitute, resync */ + break; + } + cp = cp << 6 | (text[i + n++] & 0x3Fu); + } + put_scalar(cp); + i += n; + } + show_cursor(); +} + +void con_pcg_clear(void) { + top = 0; + col = row = 0; + set_scroll(); + for (uint32_t y = 0; y < CON_PCG_ROWS; ++y) + clear_row(y); + show_cursor(); +} + +fnd_status con_pcg_init(void) { + MMIO8(VDP_DISPLAY_MODE) = 1; + MMIO8(PCG_DISABLE) = 0; + MMIO8(PCG_SCREEN_MODE) = 1; + MMIO8(PCG_SCROLL_X_LO) = 0; + MMIO8(PCG_SCROLL_X_HI) = 0; + MMIO8(PCG_CURSOR_ENABLE) = 1; + MMIO8(PCG_CURSOR_BLINK) = 10; + con_pcg_clear(); + return FND_OK; +} + +/* Keyboard state from the HID usages (page 0x07) of PeC KEY events. */ +#define HID_ENTER 40u +#define HID_ESC 41u +#define HID_BACKSPACE 42u +#define HID_TAB 43u +#define HID_CAPS_LOCK 57u +#define HID_HOME 74u +#define HID_DELETE 76u +#define HID_END 77u +#define HID_RIGHT 79u +#define HID_LEFT 80u +#define HID_DOWN 81u +#define HID_UP 82u +#define HID_KP_ENTER 88u +#define HID_LEFT_CTRL 224u + +static uint32_t held; /* bit n: HID_LEFT_CTRL + n is down */ +static uint32_t caps_lock; + +static uint32_t mods(void) { + uint32_t m = 0; + if (held & 0x22u) + m |= FND_MOD_SHIFT; + if (held & 0x11u) + m |= FND_MOD_CTRL; + if (held & 0x44u) + m |= FND_MOD_ALT; + return m; +} + +/* US layout: usages 30..56 without and with Shift (0 = no character). */ +static const char plain[] = "1234567890\0\0\0\0 -=[]\\\0;'`,./"; +static const char shifted[] = "!@#$%^&*()\0\0\0\0 _+{}|\0:\"~<>?"; + +static uint32_t key_code(uint32_t usage) { + switch (usage) { + case HID_ENTER: + case HID_KP_ENTER: + return FND_KEY_ENTER; + case HID_BACKSPACE: + return FND_KEY_BACKSPACE; + case HID_DELETE: + return FND_KEY_DELETE; + case HID_LEFT: + return FND_KEY_LEFT; + case HID_RIGHT: + return FND_KEY_RIGHT; + case HID_UP: + return FND_KEY_UP; + case HID_DOWN: + return FND_KEY_DOWN; + case HID_HOME: + return FND_KEY_HOME; + case HID_END: + return FND_KEY_END; + case HID_TAB: + return FND_KEY_TAB; + case HID_ESC: + return FND_KEY_ESC; + default: + return 0; + } +} + +static uint32_t key_char(uint32_t usage, uint32_t m) { + uint32_t shift = (m & FND_MOD_SHIFT) != 0; + if (usage >= 4u && usage <= 29u) { + uint32_t ch = 'a' + usage - 4u; + /* §8.3: Ctrl with a letter reports the letter itself (Ctrl+C = 'c'). */ + return !(m & FND_MOD_CTRL) && (shift ^ caps_lock) ? ch - 32u : ch; + } + if (usage >= 30u && usage <= 56u) + return (uint8_t)(shift ? shifted : plain)[usage - 30u]; + return 0; +} + +/* Turns one PeC event into a key press; 0 if it is none. */ +static int decode(uint32_t event, fnd_key_t *key) { + if ((event >> 28) != 0 || ((event >> 24) & 0xFu) != 0) + return 0; /* not a keyboard KEY event */ + uint32_t usage = (event >> 16) & 0xFFu, down = (event & 0xFFFFu) != 0; + if (usage >= HID_LEFT_CTRL && usage < HID_LEFT_CTRL + 8u) { + uint32_t bit = 1u << (usage - HID_LEFT_CTRL); + held = down ? held | bit : held & ~bit; + return 0; + } + if (!down) + return 0; + if (usage == HID_CAPS_LOCK) { + caps_lock ^= 1u; + return 0; + } + uint32_t m = mods(), code = key_code(usage), ch = 0; + if (!code) { + ch = key_char(usage, m); + if (!ch) + return 0; + code = FND_KEY_CHAR; + } + key->code = code; + key->ch = ch; + key->mods = m; + return 1; +} + +/* No irq_bind yet, so the FIFO is polled, sleeping between empty polls. */ +void con_pcg_read_key(fnd_key_t *key) { + for (;;) { + uint32_t status = MMIO32(PEC_EVENT_STATUS); + if (status & 2u) + MMIO32(PEC_EVENT_STATUS) = 2u; /* overflow: W1C, keep reading */ + if (!(status & 1u)) { + fnd_sys_sleep(POLL_TICKS); + continue; + } + uint32_t event = MMIO32(PEC_EVENT_DATA); + MMIO32(PEC_EVENT_POP) = 1u; + if (decode(event, key)) + return; + } +} diff --git a/foundation/lib/fnd/con_pcg.h b/foundation/lib/fnd/con_pcg.h new file mode 100644 index 0000000..201879c --- /dev/null +++ b/foundation/lib/fnd/con_pcg.h @@ -0,0 +1,30 @@ +#ifndef FOUNDATION_CON_PCG_H +#define FOUNDATION_CON_PCG_H +#include + +#include + +/* Provisional console on the PCG and the PeC keyboard, linked into the + * program until the console service exists (doc/spec_Foundation_API.md §8). + * Output follows §8.1 and keys follow the CON_READ_KEY codes of §8.3. The + * process needs MMIO grants for CON_PCG_GRANTS. */ + +#define CON_PCG_COLS 80u +#define CON_PCG_ROWS 30u + +/* MMIO the console touches: the VDP window (display and PCG registers), the + * PCG text and attribute planes in VRAM, and the PeC input window. */ +#define CON_PCG_VDP_START 0x80000000u +#define CON_PCG_VDP_END 0x80010000u +#define CON_PCG_TEXT_START 0x10000000u +#define CON_PCG_TEXT_END 0x10003000u +#define CON_PCG_INPUT_START 0x80040030u +#define CON_PCG_INPUT_END 0x80040180u + +fnd_status con_pcg_init(void); +void con_pcg_write(const uint8_t *text, uint32_t len); +void con_pcg_clear(void); +/* Waits for the next key press. */ +void con_pcg_read_key(fnd_key_t *key); + +#endif diff --git a/foundation/lib/fnd/start.c b/foundation/lib/fnd/start.c new file mode 100644 index 0000000..24330e1 --- /dev/null +++ b/foundation/lib/fnd/start.c @@ -0,0 +1,71 @@ +/* libfnd startup, roles and streams (doc/spec_Foundation_API.md §11.2, + * §11.3, §11.5). Provisional, see : proc_startup is not + * implemented yet, so fnd_init continues with argc = 0 as §11.2 says for a + * missing startup block, and the console roles are the linked-in console. */ +#include +#include + +#include "con_pcg.h" + +static fnd_handle console_role; + +fnd_status fnd_init(void) { + fnd_status status = con_pcg_init(); + if (status == FND_OK) + console_role = FND_LOCAL_CONSOLE; + return status; +} + +uint32_t fnd_argc(void) { return 0; } +const char *fnd_argv(uint32_t i) { + (void)i; + return 0; +} +fnd_handle fnd_std_in(void) { return console_role; } +fnd_handle fnd_std_out(void) { return console_role; } +fnd_handle fnd_std_err(void) { return console_role; } +fnd_handle fnd_console(void) { return console_role; } +fnd_handle fnd_fs(void) { return 0; } + +void fnd_exit(int32_t code) { + for (;;) + fnd_sys_proc_exit(code); +} + +fnd_status fnd_write(fnd_handle h, const void *buf, uint32_t len, uint32_t *put) { + if (h == 0 || h != console_role) + return FND_E_BAD_HANDLE; + con_pcg_write((const uint8_t *)buf, len); + if (put) + *put = len; + return FND_OK; +} + +fnd_status fnd_con_write(const char *text) { + uint32_t len = 0; + while (text[len]) + ++len; + return fnd_write(fnd_std_out(), text, len, 0); +} + +fnd_status fnd_con_getkey(fnd_key_t *key) { + if (!fnd_console()) + return FND_E_BAD_HANDLE; + con_pcg_read_key(key); + return FND_OK; +} + +fnd_status fnd_con_clear(void) { + if (!fnd_console()) + return FND_E_BAD_HANDLE; + con_pcg_clear(); + return FND_OK; +} + +fnd_status fnd_con_size(uint32_t *cols, uint32_t *rows) { + if (!fnd_console()) + return FND_E_BAD_HANDLE; + *cols = CON_PCG_COLS; + *rows = CON_PCG_ROWS; + return FND_OK; +} diff --git a/foundation/lib/fnd/strerror.c b/foundation/lib/fnd/strerror.c new file mode 100644 index 0000000..dfb82a0 --- /dev/null +++ b/foundation/lib/fnd/strerror.c @@ -0,0 +1,64 @@ +/* fnd_strerror (doc/spec_Foundation_API.md §11.9): the kernel codes as in + * doc/spec_Foundation_shell.md §11.1, then the API codes. */ +#include + +const char *fnd_strerror(fnd_status code) { + switch (code) { + case FND_E_BAD_ARG: + return "invalid argument"; + case FND_E_BAD_HANDLE: + return "bad handle"; + case FND_E_ACCESS: + return "access denied"; + case FND_E_NO_MEMORY: + return "out of memory"; + case FND_E_LIMIT: + return "limit exceeded"; + case FND_E_PEER_GONE: + return "service gone"; + case FND_E_NOT_FOUND: + return "not found"; + case FND_E_EXISTS: + return "already exists"; + case FND_E_KILLED: + return "interrupted"; + case FND_E_FORMAT: + return "bad format"; + case FND_E_UNSUPPORTED: + return "not supported"; + case FND_E_ADDRESS_CONFLICT: + return "slot in use"; + case FND_E_REGION_LIMIT: + return "too many regions"; + case FND_E_IO: + return "I/O error"; + case FND_E_NO_MEDIA: + return "no media"; + case FND_E_READ_ONLY: + return "read-only"; + case FND_E_BUSY: + return "busy"; + case FND_E_DEPENDENCY: + return "missing dependency"; + case FND_E_NOT_DIR: + return "not a directory"; + case FND_E_IS_DIR: + return "is a directory"; + case FND_E_NOT_EMPTY: + return "directory not empty"; + case FND_E_BAD_PATH: + return "bad path"; + case FND_E_NAME_TOO_LONG: + return "name too long"; + case FND_E_NO_SPACE: + return "no space"; + case FND_E_CROSS_DEVICE: + return "cross-device move"; + case FND_E_INTERRUPTED: + return "interrupted"; + case FND_E_NEEDS_FSCK: + return "needs fsck"; + default: + return "error"; + } +} diff --git a/foundation/shell/shell.c b/foundation/shell/shell.c new file mode 100644 index 0000000..9585a89 --- /dev/null +++ b/foundation/shell/shell.c @@ -0,0 +1,1044 @@ +/* Foundation shell 1.0 (doc/spec_Foundation_shell.md), the part that runs + * on the current kernel: lexing and syntax (§3, §4), variables (§5.1), the + * built-in commands that need no file system (§10), messages (§11) and the + * interactive line editor (§9). + * + * Not yet possible, and reported as FND_E_UNSUPPORTED or "command not found" + * (README.md lists them): the fs service and libfnd file calls (file + * redirection, wildcards, CD to a path, DIR/TYPE/COPY/DEL/REN/MD/RD, scripts, + * /etc/profile.fsh and PATH lookup), proc_spawn (external programs) and the + * Ctrl+C break thread (thread_create, CON_BREAK_WAIT). */ +#include + +#include +#include + +#define SHELL_VERSION "1.0" +#define LINE_MAX 1023u +#define WORDS_MAX 64u +#define VARS_MAX 64u +#define NAME_MAX 31u +#define VALUE_MAX 1023u +#define HISTORY_MAX 16u +#define TOKENS_MAX 1024u +#define SEGS_MAX 1024u +#define POOL_MAX 2048u +#define ARGS_MAX 8192u +#define CMDS_MAX 512u + +/* ---- strings ---- */ + +static uint32_t str_len(const char *s) { + uint32_t n = 0; + while (s[n]) + ++n; + return n; +} + +static uint32_t ascii_upper(uint32_t c) { return c >= 'a' && c <= 'z' ? c - 32u : c; } + +/* ASCII case-insensitive comparison of [a, a + na) and [b, b + nb). */ +static int name_cmp(const char *a, uint32_t na, const char *b, uint32_t nb) { + for (uint32_t i = 0; i < na && i < nb; ++i) { + uint32_t x = ascii_upper((uint8_t)a[i]), y = ascii_upper((uint8_t)b[i]); + if (x != y) + return x < y ? -1 : 1; + } + return na == nb ? 0 : na < nb ? -1 : 1; +} + +static int name_is(const char *word, const char *name) { + return name_cmp(word, str_len(word), name, str_len(name)) == 0; +} + +static void copy_bytes(char *dst, const char *src, uint32_t n) { + for (uint32_t i = 0; i < n; ++i) + dst[i] = src[i]; +} + +static uint32_t format_i32(int32_t value, char *out) { + char digits[11]; + uint32_t n = 0, len = 0, v = value < 0 ? 0u - (uint32_t)value : (uint32_t)value; + do { + digits[n++] = (char)('0' + v % 10u); + v /= 10u; + } while (v); + if (value < 0) + out[len++] = '-'; + while (n) + out[len++] = digits[--n]; + out[len] = 0; + return len; +} + +/* Decimal i32 (§10 EXIT); 0 if text is not one. */ +static int parse_i32(const char *text, int32_t *out) { + uint32_t i = 0, negative = 0, value = 0; + if (text[0] == '-' || text[0] == '+') + negative = text[i++] == '-'; + if (!text[i]) + return 0; + for (; text[i]; ++i) { + uint32_t d = (uint8_t)text[i] - '0'; + if (d > 9u || value > (0x80000000u - d) / 10u) + return 0; + value = value * 10u + d; + } + if (!negative && value > 0x7FFFFFFFu) + return 0; + *out = negative ? (int32_t)(0u - value) : (int32_t)value; + return 1; +} + +/* ---- output ---- */ + +static void put(fnd_handle h, const char *s, uint32_t n) { + if (n) + (void)fnd_write(h, s, n, 0); +} +static void out(const char *s) { put(fnd_std_out(), s, str_len(s)); } +static void out_n(const char *s, uint32_t n) { put(fnd_std_out(), s, n); } +static void err(const char *s) { put(fnd_std_err(), s, str_len(s)); } + +static void out_dec(int32_t value, uint32_t width) { + char text[12]; + uint32_t n = format_i32(value, text); + while (n < width--) + out(" "); + out(text); +} + +/* §11.1: ": : ()", target optional. */ +static int32_t report(const char *command, const char *target, int32_t code) { + char text[12]; + err(command); + err(": "); + if (target) { + err(target); + err(": "); + } + err(fnd_strerror(code)); + err(" ("); + format_i32(code, text); + err(text); + err(")\n"); + return code; +} + +static int32_t usage(const char *text) { + err("usage: "); + err(text); + err("\n"); + return FND_E_BAD_ARG; +} + +/* ---- variables (§5.1) ---- */ + +struct var { + uint32_t name_len, value_len; + char name[NAME_MAX + 1u]; + char value[VALUE_MAX + 1u]; +}; +static struct var vars[VARS_MAX]; +static uint32_t var_count; +static int32_t last_status; /* $? */ + +static struct var *var_find(const char *name, uint32_t len) { + for (uint32_t i = 0; i < var_count; ++i) { + if (name_cmp(vars[i].name, vars[i].name_len, name, len) == 0) + return &vars[i]; + } + return 0; +} + +static int name_start(uint32_t c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; } +static int name_char(uint32_t c) { return name_start(c) || (c >= '0' && c <= '9'); } + +static int valid_name(const char *name, uint32_t len) { + if (len == 0 || len > NAME_MAX || !name_start((uint8_t)name[0])) + return 0; + for (uint32_t i = 1; i < len; ++i) { + if (!name_char((uint8_t)name[i])) + return 0; + } + return 1; +} + +/* An empty value removes the variable. The list stays sorted by name. */ +static int32_t var_set(const char *name, uint32_t len, const char *value, uint32_t value_len) { + if (value_len > VALUE_MAX) + return FND_E_LIMIT; + struct var *v = var_find(name, len); + if (value_len == 0) { + if (v) { + uint32_t i = (uint32_t)(v - vars); + for (; i + 1u < var_count; ++i) + vars[i] = vars[i + 1u]; + --var_count; + } + return FND_OK; + } + if (!v) { + if (var_count == VARS_MAX) + return FND_E_LIMIT; + uint32_t i = var_count++; + for (; i && name_cmp(vars[i - 1u].name, vars[i - 1u].name_len, name, len) > 0; --i) + vars[i] = vars[i - 1u]; + v = &vars[i]; + copy_bytes(v->name, name, len); + v->name[len] = 0; + v->name_len = len; + } + copy_bytes(v->value, value, value_len); + v->value[value_len] = 0; + v->value_len = value_len; + return FND_OK; +} + +/* ---- lexing (§3) ---- */ + +enum { TK_WORD, TK_SEMI, TK_AND, TK_OR, TK_IN, TK_OUT, TK_APPEND, TK_ERR, TK_ERR_APPEND, TK_ERR_DUP }; +enum { SEG_TEXT, SEG_VAR, SEG_SPECIAL }; + +/* A word is a run of segments: text bytes in pool, or a variable whose name + * is in pool (SEG_SPECIAL: one of ?, #, 0-9). */ +struct seg { + uint16_t kind, start, len; +}; +struct token { + uint16_t kind, first, count, glob; /* glob: an unquoted * or ? */ +}; + +static struct token tokens[TOKENS_MAX]; +static struct seg segs[SEGS_MAX]; +static char pool[POOL_MAX]; +static uint32_t token_count, seg_count, pool_len; +static const char *lex_error; /* target text of a lexing error */ + +static int add_text(struct token *t, const char *text, uint32_t len) { + if (pool_len + len > POOL_MAX) + return 0; + struct seg *last = t->count ? &segs[seg_count - 1u] : 0; + if (last && last->kind == SEG_TEXT && last->start + last->len == pool_len) { + last->len = (uint16_t)(last->len + len); + } else { + if (seg_count == SEGS_MAX) + return 0; + segs[seg_count++] = (struct seg){SEG_TEXT, (uint16_t)pool_len, (uint16_t)len}; + ++t->count; + } + copy_bytes(pool + pool_len, text, len); + pool_len += len; + return 1; +} + +static int add_var(struct token *t, uint32_t kind, const char *name, uint32_t len) { + if (seg_count == SEGS_MAX || pool_len + len > POOL_MAX) + return 0; + segs[seg_count++] = (struct seg){(uint16_t)kind, (uint16_t)pool_len, (uint16_t)len}; + ++t->count; + copy_bytes(pool + pool_len, name, len); + pool_len += len; + return 1; +} + +/* At line[*pos] == '$'. A '$' that starts no reference is a literal. */ +static int32_t lex_dollar(const char *line, uint32_t len, uint32_t *pos, struct token *t) { + uint32_t p = *pos + 1u; + uint32_t c = p < len ? (uint8_t)line[p] : 0; + if (c == '?' || c == '#' || (c >= '0' && c <= '9')) { + *pos = p + 1u; + return add_var(t, SEG_SPECIAL, line + p, 1) ? FND_OK : FND_E_LIMIT; + } + if (c == '{') { + uint32_t start = ++p; + while (p < len && line[p] != '}') + ++p; + uint32_t n = p - start; + if (p == len) { + lex_error = "unterminated ${"; + return FND_E_BAD_ARG; + } + *pos = p + 1u; + if (n == 1u && (line[start] == '?' || line[start] == '#' || + (line[start] >= '0' && line[start] <= '9'))) + return add_var(t, SEG_SPECIAL, line + start, 1) ? FND_OK : FND_E_LIMIT; + if (!valid_name(line + start, n)) { + lex_error = "bad variable name"; + return FND_E_BAD_ARG; + } + return add_var(t, SEG_VAR, line + start, n) ? FND_OK : FND_E_LIMIT; + } + if (name_start(c)) { + uint32_t start = p; + while (p < len && name_char((uint8_t)line[p])) + ++p; + if (p - start > NAME_MAX) { + lex_error = "bad variable name"; + return FND_E_BAD_ARG; + } + *pos = p; + return add_var(t, SEG_VAR, line + start, p - start) ? FND_OK : FND_E_LIMIT; + } + *pos = p; + return add_text(t, "$", 1) ? FND_OK : FND_E_LIMIT; +} + +static int ends_word(uint32_t c) { + return c == ' ' || c == '\t' || c == ';' || c == '&' || c == '|' || c == '<' || c == '>' || + c == '(' || c == ')' || c == '`'; +} + +static int32_t lex_word(const char *line, uint32_t len, uint32_t *pos, struct token *t) { + uint32_t p = *pos; + int32_t status; + while (p < len && !ends_word((uint8_t)line[p])) { + char c = line[p]; + if (c == '\'') { + uint32_t start = ++p; + while (p < len && line[p] != '\'') + ++p; + if (p == len) { + lex_error = "unterminated quote"; + return FND_E_BAD_ARG; + } + if (!add_text(t, line + start, p - start)) + return FND_E_LIMIT; + ++p; + } else if (c == '"') { + ++p; + for (;;) { + if (p == len) { + lex_error = "unterminated quote"; + return FND_E_BAD_ARG; + } + char q = line[p]; + if (q == '"') { + ++p; + break; + } + if (q == '\\' && p + 1u < len && + (line[p + 1u] == '"' || line[p + 1u] == '\\' || line[p + 1u] == '$')) { + if (!add_text(t, line + p + 1u, 1)) + return FND_E_LIMIT; + p += 2u; + } else if (q == '$') { + if ((status = lex_dollar(line, len, &p, t)) != FND_OK) + return status; + } else { + if (!add_text(t, line + p, 1)) + return FND_E_LIMIT; + ++p; + } + } + } else if (c == '\\') { + if (p + 1u == len) { + lex_error = "trailing \\"; + return FND_E_BAD_ARG; + } + if (!add_text(t, line + p + 1u, 1)) + return FND_E_LIMIT; + p += 2u; + } else if (c == '$') { + if ((status = lex_dollar(line, len, &p, t)) != FND_OK) + return status; + } else { + if (c == '*' || c == '?') + t->glob = 1; + if (!add_text(t, line + p, 1)) + return FND_E_LIMIT; + ++p; + } + } + *pos = p; + return FND_OK; +} + +static int32_t push_token(uint32_t kind) { + if (token_count == TOKENS_MAX) + return FND_E_LIMIT; + tokens[token_count++] = (struct token){(uint16_t)kind, (uint16_t)seg_count, 0, 0}; + return FND_OK; +} + +static int32_t lex(const char *line, uint32_t len) { + token_count = seg_count = pool_len = 0; + lex_error = 0; + uint32_t p = 0; + while (p < len) { + char c = line[p]; + char n1 = p + 1u < len ? line[p + 1u] : 0, n2 = p + 2u < len ? line[p + 2u] : 0; + char n3 = p + 3u < len ? line[p + 3u] : 0; + uint32_t kind, width = 1; + if (c == ' ' || c == '\t') { + ++p; + continue; + } + if (c == '#') + break; /* a comment: '#' at the start of a word */ + if (c == ';') { + kind = TK_SEMI; + } else if (c == '&' && n1 == '&') { + kind = TK_AND, width = 2; + } else if (c == '|' && n1 == '|') { + kind = TK_OR, width = 2; + } else if (c == '&' || c == '|' || c == '(' || c == ')' || c == '`') { + static char reserved[4] = "'x'"; + reserved[1] = c; + lex_error = reserved; + return FND_E_UNSUPPORTED; + } else if (c == '<') { + kind = TK_IN; + } else if (c == '>') { + kind = n1 == '>' ? TK_APPEND : TK_OUT, width = n1 == '>' ? 2u : 1u; + } else if (c == '2' && n1 == '>') { + if (n2 == '>') + kind = TK_ERR_APPEND, width = 3; + else if (n2 == '&' && n3 == '1') + kind = TK_ERR_DUP, width = 4; + else + kind = TK_ERR, width = 2; + } else { + int32_t status = push_token(TK_WORD); + if (status == FND_OK) + status = lex_word(line, len, &p, &tokens[token_count - 1u]); + if (status != FND_OK) + return status; + continue; + } + int32_t status = push_token(kind); + if (status != FND_OK) + return status; + p += width; + } + return FND_OK; +} + +/* ---- syntax (§4) ---- */ + +enum { LINK_SEQ, LINK_AND, LINK_OR }; + +struct command { + uint16_t link; /* how it follows the previous command */ + uint16_t first, count; /* its tokens, words and redirections */ +}; +static struct command cmds[CMDS_MAX]; +static uint32_t cmd_count; + +static int is_redirect(uint32_t kind) { return kind >= TK_IN; } + +/* Checks the whole line before anything runs (a syntax error runs nothing). */ +static int32_t parse(void) { + uint32_t link = LINK_SEQ, words = 0, start = 0; + cmd_count = 0; + for (uint32_t i = 0; i <= token_count; ++i) { + uint32_t kind = i < token_count ? tokens[i].kind : TK_SEMI; + if (kind == TK_WORD) { + ++words; + continue; + } + if (is_redirect(kind)) { + if (kind != TK_ERR_DUP) { + if (i + 1u == token_count || tokens[i + 1u].kind != TK_WORD) { + lex_error = "missing redirection target"; + return FND_E_BAD_ARG; + } + ++i; /* the target is not a command word */ + } + continue; + } + /* ;, &&, || or the end of the line */ + if (words == 0) { + int at_end = i == token_count; + if (at_end && start == i && (cmd_count == 0 || link == LINK_SEQ)) + return FND_OK; /* empty line, or a trailing ';' */ + lex_error = "syntax error"; + return FND_E_BAD_ARG; + } + if (cmd_count == CMDS_MAX) + return FND_E_LIMIT; + cmds[cmd_count++] = (struct command){(uint16_t)link, (uint16_t)start, (uint16_t)(i - start)}; + link = kind == TK_AND ? LINK_AND : kind == TK_OR ? LINK_OR : LINK_SEQ; + words = 0; + start = i + 1u; + } + return FND_OK; +} + +/* ---- expansion (§5) ---- */ + +static char args[ARGS_MAX]; +static uint32_t args_len; + +static int append_arg(const char *text, uint32_t len) { + if (args_len + len >= ARGS_MAX) + return 0; + copy_bytes(args + args_len, text, len); + args_len += len; + return 1; +} + +/* Expands one word into args, NUL-terminated; returns its start or -1. */ +static int32_t expand(const struct token *t) { + uint32_t start = args_len; + for (uint32_t i = 0; i < t->count; ++i) { + const struct seg *s = &segs[t->first + i]; + const char *text = pool + s->start; + int ok = 1; + if (s->kind == SEG_TEXT) { + ok = append_arg(text, s->len); + } else if (s->kind == SEG_VAR) { + const struct var *v = var_find(text, s->len); + if (v) + ok = append_arg(v->value, v->value_len); + } else if (text[0] == '?') { + char number[12]; + ok = append_arg(number, format_i32(last_status, number)); + } else if (text[0] == '#') { + ok = append_arg("0", 1); /* no script is running */ + } else if (text[0] == '0') { + ok = append_arg("shell", 5); + } /* $1..$9: empty outside a script */ + if (!ok) + return -1; + } + if (!append_arg("", 1)) + return -1; + return (int32_t)start; +} + +/* ---- built-in commands (§10) ---- */ + +typedef int32_t (*builtin_fn)(uint32_t argc, char **argv); + +struct builtin { + const char *name; + builtin_fn fn; + const char *syntax; + const char *summary; +}; + +static const struct builtin *find_builtin(const char *name); +static int exit_requested; +static int32_t exit_code; + +static int32_t cmd_cd(uint32_t argc, char **argv) { + if (argc > 2u) + return usage("CD [path]"); + if (argc == 1u) { + out("/\n"); /* no fs session yet: the cwd is always / */ + return FND_OK; + } + return report("CD", argv[1], FND_E_UNSUPPORTED); +} + +/* Commands that need the fs service, which does not exist yet. */ +static int32_t cmd_needs_fs(uint32_t argc, char **argv) { + (void)argc; + const struct builtin *b = find_builtin(argv[0]); + return report(b->name, 0, FND_E_UNSUPPORTED); +} + +static int32_t cmd_cls(uint32_t argc, char **argv) { + (void)argv; + if (argc != 1u) + return usage("CLS"); + fnd_status status = fnd_con_clear(); + return status == FND_OK ? FND_OK : report("CLS", 0, status); +} + +static int32_t cmd_echo(uint32_t argc, char **argv) { + for (uint32_t i = 1; i < argc; ++i) { + if (i > 1u) + out(" "); + out(argv[i]); + } + out("\n"); + return FND_OK; +} + +static void show_var(const struct var *v) { + out_n(v->name, v->name_len); + out("="); + out_n(v->value, v->value_len); + out("\n"); +} + +static int32_t cmd_set(uint32_t argc, char **argv) { + if (argc == 1u) { + for (uint32_t i = 0; i < var_count; ++i) + show_var(&vars[i]); + return FND_OK; + } + if (argc != 2u) + return usage("SET [NAME[=value]]"); + const char *arg = argv[1]; + uint32_t n = 0; + while (arg[n] && arg[n] != '=') + ++n; + if (!valid_name(arg, n)) + return usage("SET [NAME[=value]]"); + if (!arg[n]) { + const struct var *v = var_find(arg, n); + if (!v) + return report("SET", arg, FND_E_NOT_FOUND); + out_n(v->value, v->value_len); + out("\n"); + return FND_OK; + } + char name[NAME_MAX + 1u]; + copy_bytes(name, arg, n); + name[n] = 0; + int32_t status = var_set(arg, n, arg + n + 1u, str_len(arg + n + 1u)); + return status == FND_OK ? FND_OK : report("SET", name, status); +} + +static int32_t read_sysinfo(uint32_t info[8]) { + int32_t size = fnd_sys_info(FND_INFO_SYSTEM, info, FND_SYSINFO_BYTES); + return size < 0 ? size : FND_OK; +} + +static int32_t cmd_ver(uint32_t argc, char **argv) { + (void)argv; + if (argc != 1u) + return usage("VER"); + uint32_t info[8]; + out("Foundation shell " SHELL_VERSION "\n"); + int32_t status = read_sysinfo(info); + if (status != FND_OK) + return report("VER", 0, status); + out("Kernel ABI "); + out_dec((int32_t)info[0], 0); + out("."); + out_dec((int32_t)info[1], 0); + out("\n"); + return FND_OK; +} + +static int32_t cmd_mem(uint32_t argc, char **argv) { + (void)argv; + if (argc != 1u) + return usage("MEM"); + uint32_t info[8]; + int32_t status = read_sysinfo(info); + if (status != FND_OK) + return report("MEM", 0, status); + out_dec((int32_t)(info[3] / 1024u), 8); + out(" KiB total\n"); + out_dec((int32_t)(info[4] / 1024u), 8); + out(" KiB free\n"); + return FND_OK; +} + +#define PS_MAX 16u +static uint32_t ps_buf[PS_MAX * FND_PROCINFO_BYTES / 4u]; + +static int32_t cmd_ps(uint32_t argc, char **argv) { + (void)argv; + if (argc != 1u) + return usage("PS"); + int32_t size = fnd_sys_info(FND_INFO_PROCESSES, ps_buf, sizeof ps_buf); + if (size < 0) + return report("PS", 0, size); + if ((uint32_t)size > sizeof ps_buf) + size = (int32_t)sizeof ps_buf; + static const char *const states[3] = {"RUN ", "BLOCK", "EXIT "}; + out(" PID PARENT STATE PRIO MEM NAME\n"); + for (uint32_t off = 0; off + FND_PROCINFO_BYTES <= (uint32_t)size; off += FND_PROCINFO_BYTES) { + const uint32_t *p = ps_buf + off / 4u; + char name[17]; + for (uint32_t i = 0; i < 16u; ++i) + name[i] = (char)(p[8u + i / 4u] >> (24u - 8u * (i % 4u))); + name[16] = 0; + out_dec((int32_t)p[0], 5); + out_dec((int32_t)p[1], 7); + out(" "); + out(p[2] < 3u ? states[p[2]] : "? "); + out_dec((int32_t)p[3], 5); + out_dec((int32_t)(p[5] / 1024u), 7); + out("K "); + out(name); + out("\n"); + } + return FND_OK; +} + +static int32_t cmd_help(uint32_t argc, char **argv); + +static int32_t cmd_exit(uint32_t argc, char **argv) { + int32_t code = last_status; + if (argc > 2u || (argc == 2u && !parse_i32(argv[1], &code))) + return usage("EXIT [code]"); + exit_requested = 1; + exit_code = code; + return code; +} + +static const struct builtin builtins[] = { + {"CD", cmd_cd, "CD [path]", "show or change the current directory"}, + {"DIR", cmd_needs_fs, "DIR [-b] [path...]", "list a directory"}, + {"TYPE", cmd_needs_fs, "TYPE path...", "write files to the output"}, + {"COPY", cmd_needs_fs, "COPY [-f] src... dst", "copy files"}, + {"DEL", cmd_needs_fs, "DEL path...", "delete files"}, + {"REN", cmd_needs_fs, "REN old new", "rename or move a file"}, + {"MD", cmd_needs_fs, "MD path...", "make directories"}, + {"MKDIR", cmd_needs_fs, "MKDIR path...", "make directories"}, + {"RD", cmd_needs_fs, "RD path...", "remove empty directories"}, + {"RMDIR", cmd_needs_fs, "RMDIR path...", "remove empty directories"}, + {"CLS", cmd_cls, "CLS", "clear the screen"}, + {"ECHO", cmd_echo, "ECHO [word...]", "write words"}, + {"SET", cmd_set, "SET [NAME[=value]]", "show or set variables"}, + {"VER", cmd_ver, "VER", "show versions"}, + {"MEM", cmd_mem, "MEM", "show memory"}, + {"PS", cmd_ps, "PS", "list processes"}, + {"HELP", cmd_help, "HELP [command]", "show help"}, + {"EXIT", cmd_exit, "EXIT [code]", "leave the shell"}, +}; +#define BUILTINS (sizeof builtins / sizeof builtins[0]) + +static const struct builtin *find_builtin(const char *name) { + for (uint32_t i = 0; i < BUILTINS; ++i) { + if (name_is(name, builtins[i].name)) + return &builtins[i]; + } + return 0; +} + +static int32_t cmd_help(uint32_t argc, char **argv) { + if (argc > 2u) + return usage("HELP [command]"); + if (argc == 2u) { + const struct builtin *b = find_builtin(argv[1]); + if (!b) + return report("HELP", argv[1], FND_E_NOT_FOUND); + out("usage: "); + out(b->syntax); + out("\n"); + return FND_OK; + } + for (uint32_t i = 0; i < BUILTINS; ++i) { + uint32_t n = str_len(builtins[i].name); + out(builtins[i].name); + while (n++ < 8u) + out(" "); + out(builtins[i].summary); + out("\n"); + } + return FND_OK; +} + +/* ---- execution (§6, §7) ---- */ + +static int32_t run_command(const struct command *c) { + char *argv[WORDS_MAX + 1u]; + uint32_t argc = 0; + int32_t redirect_target = -1; + args_len = 0; + for (uint32_t i = 0; i < c->count; ++i) { + const struct token *t = &tokens[c->first + i]; + if (is_redirect(t->kind)) { + if (t->kind == TK_ERR_DUP) + continue; /* STDERR is already the console STDOUT */ + int32_t at = expand(&tokens[c->first + ++i]); + if (at < 0) + return report("shell", 0, FND_E_LIMIT); + if (redirect_target < 0) + redirect_target = at; + continue; + } + if (argc == WORDS_MAX) + return report("shell", "too many words", FND_E_LIMIT); + int32_t at = expand(t); + if (at < 0) + return report("shell", 0, FND_E_LIMIT); + argv[argc++] = args + at; + if (t->glob) /* §5.2 needs fs directory listings */ + return report("shell", args + at, FND_E_UNSUPPORTED); + } + argv[argc] = 0; + /* §6: opening files needs the fs service. */ + if (redirect_target >= 0) + return report(argv[0], args + redirect_target, FND_E_UNSUPPORTED); + int has_slash = 0; + for (const char *p = argv[0]; *p; ++p) + has_slash |= *p == '/'; + const struct builtin *b = has_slash ? 0 : find_builtin(argv[0]); + if (!b) { + /* §7.1 steps 1 and 3 need the fs service: nothing can be found. */ + err(argv[0]); + err(": command not found ("); + char code[12]; + format_i32(FND_E_NOT_FOUND, code); + err(code); + err(")\n"); + return FND_E_NOT_FOUND; + } + return b->fn(argc, argv); +} + +static int valid_utf8(const char *s, uint32_t len) { + for (uint32_t i = 0; i < len;) { + uint32_t b = (uint8_t)s[i], need, min; + if (b < 0x80u) { + ++i; + continue; + } + if ((b & 0xE0u) == 0xC0u) + need = 1, min = 0x80u, b &= 0x1Fu; + else if ((b & 0xF0u) == 0xE0u) + need = 2, min = 0x800u, b &= 0x0Fu; + else if ((b & 0xF8u) == 0xF0u) + need = 3, min = 0x10000u, b &= 0x07u; + else + return 0; + for (uint32_t k = 1; k <= need; ++k) { + if (i + k >= len || ((uint8_t)s[i + k] & 0xC0u) != 0x80u) + return 0; + b = b << 6 | ((uint8_t)s[i + k] & 0x3Fu); + } + if (b < min || b > 0x10FFFFu || (b >= 0xD800u && b <= 0xDFFFu)) + return 0; + i += need + 1u; + } + return 1; +} + +static void run_line(const char *line, uint32_t len) { + for (uint32_t i = 0; i < len; ++i) { + if (!line[i]) { + last_status = report("shell", "NUL in line", FND_E_BAD_ARG); + return; + } + } + if (len > LINE_MAX) { + last_status = report("shell", "line too long", FND_E_LIMIT); + return; + } + if (!valid_utf8(line, len)) { + last_status = report("shell", "bad UTF-8", FND_E_BAD_ARG); + return; + } + int32_t status = lex(line, len); + if (status == FND_OK) + status = parse(); + if (status != FND_OK) { + last_status = report("shell", lex_error, status); + return; + } + for (uint32_t i = 0; i < cmd_count && !exit_requested; ++i) { + if ((cmds[i].link == LINK_AND && last_status != 0) || (cmds[i].link == LINK_OR && last_status == 0)) + continue; /* not run: $? stays */ + last_status = run_command(&cmds[i]); + } +} + +/* ---- interactive input (§9) ---- */ + +static char line[LINE_MAX + 1u]; +static uint32_t line_len, cursor; +static char history[HISTORY_MAX][LINE_MAX + 1u]; +static uint32_t history_len[HISTORY_MAX], history_count; + +static int continuation(char c) { return ((uint8_t)c & 0xC0u) == 0x80u; } + +/* Screen cells of line[from, to): one per scalar value. */ +static uint32_t cells(uint32_t from, uint32_t to) { + uint32_t n = 0; + for (uint32_t i = from; i < to; ++i) + n += !continuation(line[i]); + return n; +} + +static void back(uint32_t count) { + while (count--) + out("\b"); +} + +static uint32_t prev_scalar(uint32_t at) { + do + --at; + while (at && continuation(line[at])); + return at; +} + +static uint32_t next_scalar(uint32_t at) { + do + ++at; + while (at < line_len && continuation(line[at])); + return at; +} + +/* Rewrites the screen from the cursor after line[cursor..] changed: the new + * tail, blanks over erased cells, then back to the cursor. */ +static void redraw_tail(uint32_t erased) { + out_n(line + cursor, line_len - cursor); + for (uint32_t i = 0; i < erased; ++i) + out(" "); + back(cells(cursor, line_len) + erased); +} + +static void replace_line(const char *text, uint32_t len) { + uint32_t old_cells = cells(0, line_len); + back(cells(0, cursor)); + copy_bytes(line, text, len); + line_len = len; + cursor = 0; + uint32_t new_cells = cells(0, line_len); + redraw_tail(old_cells > new_cells ? old_cells - new_cells : 0); + out_n(line, line_len); + cursor = line_len; +} + +static void encode(uint32_t cp, char *buf, uint32_t *n) { + if (cp < 0x80u) { + buf[0] = (char)cp, *n = 1; + } else if (cp < 0x800u) { + buf[0] = (char)(0xC0u | cp >> 6), buf[1] = (char)(0x80u | (cp & 0x3Fu)), *n = 2; + } else if (cp < 0x10000u) { + buf[0] = (char)(0xE0u | cp >> 12), buf[1] = (char)(0x80u | (cp >> 6 & 0x3Fu)); + buf[2] = (char)(0x80u | (cp & 0x3Fu)), *n = 3; + } else { + buf[0] = (char)(0xF0u | cp >> 18), buf[1] = (char)(0x80u | (cp >> 12 & 0x3Fu)); + buf[2] = (char)(0x80u | (cp >> 6 & 0x3Fu)), buf[3] = (char)(0x80u | (cp & 0x3Fu)), *n = 4; + } +} + +static void remember(void) { + if (line_len == 0) + return; + if (history_count) { + uint32_t last = history_count - 1u; + uint32_t same = history_len[last] == line_len; + for (uint32_t i = 0; same && i < line_len; ++i) + same = history[last][i] == line[i]; + if (same) + return; /* §9.2: not the same line twice in a row */ + } + if (history_count == HISTORY_MAX) { + for (uint32_t i = 1; i < HISTORY_MAX; ++i) { + copy_bytes(history[i - 1u], history[i], history_len[i]); + history_len[i - 1u] = history_len[i]; + } + --history_count; + } + copy_bytes(history[history_count], line, line_len); + history_len[history_count++] = line_len; +} + +/* Edits one line; 0 when Enter confirmed it, 1 when Ctrl+C dropped it. */ +static int read_line(void) { + static char draft[LINE_MAX + 1u]; + uint32_t draft_len = 0, browse = history_count; + line_len = cursor = 0; + for (;;) { + fnd_key_t key; + if (fnd_con_getkey(&key) != FND_OK) + fnd_exit(last_status); + switch (key.code) { + case FND_KEY_ENTER: + out_n(line + cursor, line_len - cursor); + out("\n"); + return 0; + case FND_KEY_LEFT: + if (cursor) { + cursor = prev_scalar(cursor); + back(1); + } + break; + case FND_KEY_RIGHT: + if (cursor < line_len) { + uint32_t next = next_scalar(cursor); + out_n(line + cursor, next - cursor); + cursor = next; + } + break; + case FND_KEY_HOME: + back(cells(0, cursor)); + cursor = 0; + break; + case FND_KEY_END: + out_n(line + cursor, line_len - cursor); + cursor = line_len; + break; + case FND_KEY_BACKSPACE: + if (cursor) { + uint32_t at = prev_scalar(cursor), n = cursor - at; + copy_bytes(line + at, line + cursor, line_len - cursor); + line_len -= n; + cursor = at; + back(1); + redraw_tail(1); + } + break; + case FND_KEY_DELETE: + if (cursor < line_len) { + uint32_t n = next_scalar(cursor) - cursor; + copy_bytes(line + cursor, line + cursor + n, line_len - cursor - n); + line_len -= n; + redraw_tail(1); + } + break; + case FND_KEY_UP: + if (browse) { + if (browse == history_count) { + copy_bytes(draft, line, line_len); + draft_len = line_len; + } + --browse; + replace_line(history[browse], history_len[browse]); + } + break; + case FND_KEY_DOWN: + if (browse < history_count) { + ++browse; + if (browse == history_count) + replace_line(draft, draft_len); + else + replace_line(history[browse], history_len[browse]); + } + break; + case FND_KEY_CHAR: + if (key.mods & FND_MOD_CTRL) { + if (key.ch == 'c' || key.ch == 'C') { + out_n(line + cursor, line_len - cursor); + out("^C\n"); + return 1; + } + break; + } else { + char bytes[4]; + uint32_t n; + encode(key.ch, bytes, &n); + if (line_len + n > LINE_MAX) + break; /* §9.2: the limit is not exceeded */ + for (uint32_t i = line_len; i > cursor; --i) + line[i - 1u + n] = line[i - 1u]; + copy_bytes(line + cursor, bytes, n); + line_len += n; + out_n(line + cursor, n); + cursor += n; + redraw_tail(0); + } + break; + default: + break; + } + } +} + +int main(void) { + if (fnd_init() != FND_OK || !fnd_std_in() || !fnd_std_out() || !fnd_console()) + fnd_exit(FND_E_BAD_HANDLE); + var_set("PATH", 4, "/bin", 4); + /* §2.1: no startup block yet, so always interactive; /etc/profile.fsh + * would need the fs service. */ + for (;;) { + out("/> "); /* §9.1: the cwd, which is / until there is an fs */ + if (read_line() != 0) + continue; /* Ctrl+C: $? is unchanged */ + remember(); + run_line(line, line_len); + if (exit_requested) + fnd_exit(exit_code); + } +} diff --git a/foundation/tests/shell/test_shell.py b/foundation/tests/shell/test_shell.py new file mode 100644 index 0000000..85a6fff --- /dev/null +++ b/foundation/tests/shell/test_shell.py @@ -0,0 +1,221 @@ +"""Foundation shell on src32_testbench (doc/spec_Foundation_shell.md). + +Builds the boot image with foundation/boot/build.py, types keys through the +testbench's PeC input (--type) and compares the PCG text screen (--dump-pcg). +The kernel stops the machine at STOP_TICKS with R1 = 42. + + python -m unittest discover -s foundation/tests/shell -v + +CLANG and SRC32_TESTBENCH override executable discovery. +""" +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +STOP_TICKS = 3000 +FRAMES = 260 +FIRST_FRAME = 30 # the shell is loaded and waiting for keys by then +STEP_FRAMES = 6 # between two --type batches + + +def find_testbench(): + suffix = '.exe' if os.name == 'nt' else '' + if os.environ.get('SRC32_TESTBENCH'): + return Path(os.environ['SRC32_TESTBENCH']) + for profile in ('release', 'debug'): + candidate = ROOT / 'target' / profile / ('src32_testbench' + suffix) + if candidate.is_file(): + return candidate + raise RuntimeError('Build cargo build --bin src32_testbench or set SRC32_TESTBENCH') + + +class ShellTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.harness = find_testbench() + cls.temp = tempfile.TemporaryDirectory(prefix='foundation-shell-') + cls.work = Path(cls.temp.name) + command = [sys.executable, str(ROOT / 'foundation/boot/build.py'), str(cls.work), + '--stop-ticks', str(STOP_TICKS)] + clang = os.environ.get('CLANG') or shutil.which('clang') + if clang: + command += ['--clang', clang] + result = subprocess.run(command, capture_output=True, text=True, encoding='utf-8', + errors='replace', timeout=600) + if result.returncode != 0: + raise RuntimeError(result.stdout + result.stderr) + cls.image = cls.work / 'foundation.wax' + + @classmethod + def tearDownClass(cls): + cls.temp.cleanup() + + def screen(self, *batches, step=STEP_FRAMES): + """Types each batch STEP frames after the previous one; returns the + screen rows without the trailing blank rows.""" + command = [str(self.harness), str(self.image), '42', '--cpu', 'pipeline', + '--frames', str(FRAMES), '--dump-pcg'] + for i, text in enumerate(batches): + command += ['--type', f'{FIRST_FRAME + step * i}={text}'] + result = subprocess.run(command, cwd=self.work, capture_output=True, text=True, + encoding='utf-8', errors='replace', timeout=600) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + lines = result.stdout.splitlines() + rows = lines[lines.index('PCG SCREEN BEGIN') + 1:lines.index('PCG SCREEN END')] + while rows and not rows[-1]: + rows.pop() + return rows + + def test_builtins_and_versions(self): + self.assertEqual(self.screen('echo hello world\\n', 'VeR\\n', 'echo\\n', 'mem\\n'), [ + '/> echo hello world', + 'hello world', + '/> VeR', + 'Foundation shell 1.0', + 'Kernel ABI 1.0', + '/> echo', + '', + '/> mem', + ' 16384 KiB total', + ' 10240 KiB free', + '/>', + ]) + + def test_quotes_and_variables(self): + # §3.2, §5.1: values are not split again; '...' expands nothing. + self.assertEqual(self.screen( + 'SET "MSG=hello world"\\n', + 'echo $msg ${MSG}x \'$MSG\' "[$MSG]" \\\\$MSG a"b c"d $ $?\\n', + 'set msg\\n', 'set\\n', 'set MSG=\\n', 'set msg\\n', 'echo [$NOPE] $0 $# [$1]\\n'), [ + '/> SET "MSG=hello world"', + '/> echo $msg ${MSG}x \'$MSG\' "[$MSG]" \\$MSG a"b c"d $ $?', + 'hello world hello worldx $MSG [hello world] $MSG ab cd $ 0', + '/> set msg', + 'hello world', + '/> set', + 'MSG=hello world', + 'PATH=/bin', + '/> set MSG=', + '/> set msg', + 'SET: msg: not found (-10)', + '/> echo [$NOPE] $0 $# [$1]', + '[] shell 0 []', + '/>', + ]) + + def test_conditionals_and_status(self): + # §4: && and || from the left; a command that does not run keeps $?. + self.assertEqual(self.screen( + 'foo && echo a || echo b\\n', 'nope; echo $?\\n', 'echo x || nope; echo $?\\n', + 'exit x; echo $?\\n'), [ + '/> foo && echo a || echo b', + 'foo: command not found (-10)', + 'b', + '/> nope; echo $?', + 'nope: command not found (-10)', + '-10', + '/> echo x || nope; echo $?', + 'x', + '0', + '/> exit x; echo $?', + 'usage: EXIT [code]', + '-2', + '/>', + ]) + + def test_syntax_errors_run_nothing(self): + self.assertEqual(self.screen( + 'echo "abc\\n', 'echo a; ; echo b\\n', 'echo a | b\\n', 'echo a &\\n', 'echo a &&\\n', + '> x\\n', 'echo a\\\\\\n', ' # comment\\n', 'echo a#b 2>&1; echo $?\\n'), [ + '/> echo "abc', + 'shell: unterminated quote: invalid argument (-2)', + '/> echo a; ; echo b', + 'shell: syntax error: invalid argument (-2)', + '/> echo a | b', + "shell: '|': not supported (-13)", + '/> echo a &', + "shell: '&': not supported (-13)", + '/> echo a &&', + 'shell: syntax error: invalid argument (-2)', + '/> > x', + 'shell: syntax error: invalid argument (-2)', + '/> echo a\\', + 'shell: trailing \\: invalid argument (-2)', + '/> # comment', + '/> echo a#b 2>&1; echo $?', + 'a#b', + '0', + '/>', + ]) + + def test_needs_file_system(self): + # Provisional: no fs service yet (README.md). + self.assertEqual(self.screen( + 'echo a > out.txt\\n', 'echo *.txt\\n', 'dir\\n', 'cd\\n', 'cd /bin\\n', '/bin/x.wax\\n'), [ + '/> echo a > out.txt', + 'echo: out.txt: not supported (-13)', + '/> echo *.txt', + 'shell: *.txt: not supported (-13)', + '/> dir', + 'DIR: not supported (-13)', + '/> cd', + '/', + '/> cd /bin', + 'CD: /bin: not supported (-13)', + '/> /bin/x.wax', + '/bin/x.wax: command not found (-10)', + '/>', + ]) + + def test_line_editing_and_history(self): + # §9.2: arrows, Home, End, Backspace, Delete, history and Ctrl+C. + self.assertEqual(self.screen( + 'echo wrld\\L\\L\\Lo\\n', 'xecho hi\\H\\X\\E!\\n', 'echo abcd\\b\\bef\\n', '\\n', + '\\U\\U\\n', '\\U\\U\\U\\D\\n', 'echo nope\\L\\L\\C', 'echo $?\\n'), [ + '/> echo world', + 'world', + '/> echo hi!', + 'hi!', + '/> echo abef', + 'abef', + '/>', + '/> echo hi!', + 'hi!', + '/> echo abef', + 'abef', + '/> echo nope^C', + '/> echo $?', + '0', + '/>', + ]) + + def test_cls_and_scrolling(self): + batches = ['echo first\\n', 'cls\\n'] + [f'echo line{i}\\n' for i in range(20)] + rows = self.screen(*batches, step=4) + # 20 commands of 2 rows and the prompt scroll the cleared screen by 11. + self.assertEqual(len(rows), 30) + self.assertEqual(rows[0], 'line5') + self.assertEqual(rows[-3:], ['/> echo line19', 'line19', '/>']) + self.assertNotIn('first', '\n'.join(rows)) + + def test_exit_restarts_shell(self): + # EXIT ends the shell; the boot image starts it again (as init would). + rows = self.screen('exit 3\\n', 'ps\\n', step=20) + self.assertEqual(rows[:3] + rows[4:], [ + '/> ps', + ' PID PARENT STATE PRIO MEM NAME', + ' 1 0 EXIT 2 0K shell', + '/>', + ]) + # MEM: the image (its size depends on the build) and the 16 KiB stack. + self.assertRegex(rows[3], r'^ 2 0 RUN 2 +[1-9][0-9]*K shell$') + + +if __name__ == '__main__': + unittest.main() diff --git a/foundation/tests/wax/main.c b/foundation/tests/wax/main.c index 269c50d..47e1a26 100644 --- a/foundation/tests/wax/main.c +++ b/foundation/tests/wax/main.c @@ -71,7 +71,7 @@ static uint32_t on_tick(uint32_t ticks) { if (++rounds == ROUNDS) return failures == 0u ? 42u : 0x100u + failures; /* Spawn and exit again: the slot, records and threads are reused. */ - pid_app = fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), libs, 3); + pid_app = fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), libs, 3, 0, 0); check(pid_app > 0); return 0; } @@ -79,24 +79,24 @@ static uint32_t on_tick(uint32_t ticks) { int main(void) { static const uint8_t garbage[64] = {'W', 'A', 'X', 0, 0, 3}; /* Rejections are atomic: nothing is allocated or written. */ - check(fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), base_only, 1) == FND_E_DEPENDENCY); - check(fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), old_math, 2) == FND_E_DEPENDENCY); - check(fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app_unprotected), libs, 3) == FND_E_FORMAT); - check(fnd_proc_spawn_wax("bad", FND_PRIO_APP, garbage, sizeof garbage, 0, 0) == FND_E_FORMAT); - check(fnd_proc_spawn_wax("app", FND_PRIO_IDLE, IMAGE(app), libs, 3) == FND_E_BAD_ARG); + check(fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), base_only, 1, 0, 0) == FND_E_DEPENDENCY); + check(fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), old_math, 2, 0, 0) == FND_E_DEPENDENCY); + check(fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app_unprotected), libs, 3, 0, 0) == FND_E_FORMAT); + check(fnd_proc_spawn_wax("bad", FND_PRIO_APP, garbage, sizeof garbage, 0, 0, 0, 0) == FND_E_FORMAT); + check(fnd_proc_spawn_wax("app", FND_PRIO_IDLE, IMAGE(app), libs, 3, 0, 0) == FND_E_BAD_ARG); check(slot_clear(SLOT_APP)); - pid_app = fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), libs, 3); - pid_app2 = fnd_proc_spawn_wax("app2", FND_PRIO_APP, IMAGE(app2), libs, 3); - pid_v2 = fnd_proc_spawn_wax("v2app", FND_PRIO_APP, IMAGE(v2app), 0, 0); - pid_write = fnd_proc_spawn_wax("poke-write", FND_PRIO_APP, IMAGE(poke_write), 0, 0); - pid_exec = fnd_proc_spawn_wax("poke-exec", FND_PRIO_APP, IMAGE(poke_exec), 0, 0); - if (fnd_proc_spawn_wax("spin", FND_PRIO_APP, IMAGE(spin), 0, 0) < 0) + pid_app = fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), libs, 3, 0, 0); + pid_app2 = fnd_proc_spawn_wax("app2", FND_PRIO_APP, IMAGE(app2), libs, 3, 0, 0); + pid_v2 = fnd_proc_spawn_wax("v2app", FND_PRIO_APP, IMAGE(v2app), 0, 0, 0, 0); + pid_write = fnd_proc_spawn_wax("poke-write", FND_PRIO_APP, IMAGE(poke_write), 0, 0, 0, 0); + pid_exec = fnd_proc_spawn_wax("poke-exec", FND_PRIO_APP, IMAGE(poke_exec), 0, 0, 0, 0); + if (fnd_proc_spawn_wax("spin", FND_PRIO_APP, IMAGE(spin), 0, 0, 0, 0) < 0) return 2; if (pid_app < 0 || pid_app2 < 0 || pid_v2 < 0 || pid_write < 0 || pid_exec < 0) return 2; /* One process per slot. */ - if (fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), libs, 3) != FND_E_ADDRESS_CONFLICT) + if (fnd_proc_spawn_wax("app", FND_PRIO_APP, IMAGE(app), libs, 3, 0, 0) != FND_E_ADDRESS_CONFLICT) return 3; fnd_set_tick_hook(on_tick); fnd_kernel_run(); diff --git a/src/bin/src32_testbench.rs b/src/bin/src32_testbench.rs index aab1830..4d59745 100644 --- a/src/bin/src32_testbench.rs +++ b/src/bin/src32_testbench.rs @@ -8,7 +8,7 @@ use cpt32::cpu::{CPU_PROFILE_STAGE_NAMES, CYCLES_PER_FRAME, CpuProfile}; use cpt32::cpu_select::{AnyCpu, resolve_cpu_kind, split_cpu_arg}; use cpt32::devices::pec::idc::idc::connect_idc_from_config; -use cpt32::devices::pec::pec::connect_pec_input; +use cpt32::devices::pec::pec::{HostInputEvent, connect_pec_input}; use cpt32::devices::pec::rng::connect_rng; use cpt32::music::guest::{STREAM_BASE, read_music_stream}; @@ -21,6 +21,111 @@ fn integer(text: &str) -> u32 { text.parse().expect("invalid integer") } } +/// HID keyboard usages (page 0x07, US layout) that type `text`. Escapes: +/// `\n` Enter, `\b` Backspace, `\t` Tab, `\e` Esc, `\\` a backslash, +/// `\L` `\R` `\U` `\D` arrows, `\H` Home, `\E` End, `\X` Delete, `\C` Ctrl+C. +fn typed_keys(text: &str) -> Vec { + const SHIFT: u8 = 225; + const CTRL: u8 = 224; + fn usage(c: char) -> Option<(u8, bool)> { + const PLAIN: &str = "1234567890\0\0\0\0 -=[]\\\0;'`,./"; + const SHIFTED: &str = "!@#$%^&*()\0\0\0\0 _+{}|\0:\"~<>?"; + match c { + 'a'..='z' => Some((4 + (c as u8 - b'a'), false)), + 'A'..='Z' => Some((4 + (c as u8 - b'A'), true)), + _ => PLAIN + .chars() + .position(|p| p == c) + .map(|i| (30 + i as u8, false)) + .or_else(|| { + SHIFTED + .chars() + .position(|p| p == c) + .map(|i| (30 + i as u8, true)) + }), + } + } + let mut events = Vec::new(); + let mut press = |code: u8, modifier: Option| { + if let Some(m) = modifier { + events.push(HostInputEvent::Key { + usage: m, + down: true, + }); + } + events.push(HostInputEvent::Key { + usage: code, + down: true, + }); + events.push(HostInputEvent::Key { + usage: code, + down: false, + }); + if let Some(m) = modifier { + events.push(HostInputEvent::Key { + usage: m, + down: false, + }); + } + }; + let mut chars = text.chars(); + while let Some(c) = chars.next() { + let (code, modifier) = if c == '\\' { + match chars.next().expect("--type: escape at end of text") { + 'n' => (40, None), + 'b' => (42, None), + 't' => (43, None), + 'e' => (41, None), + 'L' => (80, None), + 'R' => (79, None), + 'U' => (82, None), + 'D' => (81, None), + 'H' => (74, None), + 'E' => (77, None), + 'X' => (76, None), + 'C' => (6, Some(CTRL)), + '\\' => (49, None), + other => panic!("--type: unknown escape \\{other}"), + } + } else { + let (code, shifted) = usage(c).unwrap_or_else(|| panic!("--type: no key for {c:?}")); + (code, shifted.then_some(SHIFT)) + }; + press(code, modifier); + } + events +} + +/// The visible PCG text screen, one line per row with trailing blanks +/// removed. Follows SCREEN_MODE and SCROLL_Y; modes 0, 1 and 3 only. +fn pcg_screen(cpu: &mut AnyCpu) -> Vec { + let mode = cpu.read_mem_u8(0x8000_F003); + let (columns, cell_height) = match mode { + 0 => (40, 8), + 3 => (80, 16), + _ => (80, 8), + }; + let scroll_y = + u32::from(cpu.read_mem_u8(0x8000_F00E)) | u32::from(cpu.read_mem_u8(0x8000_F00F)) << 8; + let top = scroll_y / cell_height; + (0..30) + .map(|row| { + let ring = (top + row) % 30; + let line: String = (0..columns) + .map(|column| { + let byte = cpu.read_mem_u8(0x1000_0000 + ring * columns + column); + if (0x20..0x7F).contains(&byte) { + byte as char + } else { + ' ' + } + }) + .collect(); + line.trim_end().to_string() + }) + .collect() +} + fn gp0_noise_pixel(cpu: &mut AnyCpu, x: usize, y: usize) -> u8 { let index = cpu.read_mem_u8(0x1000_0000 + (y * 320 + x) as u32); cpu.read_mem_u8(0x1001_2C00 + u32::from(index) * 3) @@ -29,7 +134,7 @@ fn main() { cpt32::init_logging(); let args: Vec = env::args().skip(1).collect(); - let path = args.first().expect("usage: src32_testbench PROGRAM.bin [expected] [--allow-running] [--music-stream PATH] [--frames N] [--irq-drain-cycles N] [--profile-cpu] [--skip-render-checks] [--expect-mem-u8 ADDRESS=VALUE] [--cpu scalar|cached|cached-relaxed|jit|pipeline]"); + let path = args.first().expect("usage: src32_testbench PROGRAM.bin [expected] [--allow-running] [--music-stream PATH] [--frames N] [--irq-drain-cycles N] [--profile-cpu] [--skip-render-checks] [--expect-mem-u8 ADDRESS=VALUE] [--type FRAME=TEXT] [--dump-pcg] [--cpu scalar|cached|cached-relaxed|jit|pipeline]"); let (args, cli_cpu) = split_cpu_arg(&args).expect("invalid --cpu option"); let cpu_kind = resolve_cpu_kind(cli_cpu, Path::new("config.yaml")).expect("Invalid CPU configuration"); @@ -47,6 +152,8 @@ fn main() { let mut profile_cpu = false; let mut skip_render_checks = false; let mut memory_expectations = Vec::new(); + let mut typed: Vec<(usize, Vec)> = Vec::new(); + let mut dump_pcg = false; let mut options = args.iter().skip(1); while let Some(option) = options.next() { match option.as_str() { @@ -76,6 +183,15 @@ fn main() { u8::try_from(integer(value)).expect("value must fit u8"), )); } + "--type" => { + let (frame, text) = options + .next() + .expect("--type requires FRAME=TEXT") + .split_once('=') + .expect("expected FRAME=TEXT"); + typed.push((integer(frame) as usize, typed_keys(text))); + } + "--dump-pcg" => dump_pcg = true, _ if option.starts_with('-') => panic!("unknown option: {option}"), _ => expected = integer(option), } @@ -89,7 +205,7 @@ fn main() { .map(|path| read_music_stream(Path::new(path), image.len()).expect("invalid music stream")); let mut bus = Bus::new(); - let _input_sender = connect_pec_input(&mut bus); + let input_sender = connect_pec_input(&mut bus); connect_rng(&mut bus); let vdp = connect_devices_with_vdp(&mut bus); connect_idc_from_config(&mut bus).expect("Invalid disk configuration"); @@ -128,6 +244,17 @@ fn main() { let frame_count = frames.unwrap_or((cycle_budget + cycles_per_frame - 1) / cycles_per_frame); let mut cpu_profile = CpuProfile::default(); for frame in 0..frame_count { + let mut sent = false; + for (_, events) in typed.iter().filter(|(at, _)| *at == frame) { + for &event in events { + input_sender.send(event).expect("PeC input thread stopped"); + } + sent = true; + } + if sent { + // The PeC input thread applies host events asynchronously. + std::thread::sleep(std::time::Duration::from_millis(50)); + } vdp.borrow_mut().tick(); cpu.set_irq_source(0, true); cpu.set_irq_source(0, false); @@ -183,6 +310,13 @@ fn main() { if let Some(stats) = cpu.jit_stats() { println!("CPU JIT: {stats:?}"); } + if dump_pcg { + println!("PCG SCREEN BEGIN"); + for line in pcg_screen(&mut cpu) { + println!("{line}"); + } + println!("PCG SCREEN END"); + } let result = cpu.read_reg(1); assert!( allow_running || !cpu.is_running(), diff --git a/tools/llvm-src32/driver.py b/tools/llvm-src32/driver.py index 3cc25f6..c8f5399 100644 --- a/tools/llvm-src32/driver.py +++ b/tools/llvm-src32/driver.py @@ -340,16 +340,28 @@ def check_link_layout(reference, candidate, source): def protected_blocks(blocks, data_start): - """Split assembled blocks at the code/data boundary: code R/X, data R/W.""" + """Split assembled blocks at the code/data boundary: code R/X, data R/W. + + Alignment padding between the sparse assembler's blocks is filled with + zeros, so code and data stay one block each however many globals there + are (PROTECTED WAX allows at most 7 blocks).""" perm_rx, perm_rw = 1 | 4, 1 | 2 result = [] - for address, data in blocks: + for address, data in sorted(blocks, key=lambda block: block[0]): data = bytes(data) split = min(max(data_start - address, 0), len(data)) + parts = [] if split: - result.append((address, data[:split], perm_rx)) + parts.append((address, data[:split], perm_rx)) if split < len(data): - result.append((address + split, data[split:], perm_rw)) + parts.append((address + split, data[split:], perm_rw)) + for part in parts: + if result and result[-1][2] == part[2] and result[-1][0] + len(result[-1][1]) <= part[0]: + start, previous, perm = result[-1] + gap = part[0] - start - len(previous) + result[-1] = (start, previous + bytes(gap) + part[1], perm) + else: + result.append(part) return result diff --git a/tools/llvm-src32/test_toolchain.py b/tools/llvm-src32/test_toolchain.py index 07c9921..6fed417 100644 --- a/tools/llvm-src32/test_toolchain.py +++ b/tools/llvm-src32/test_toolchain.py @@ -364,6 +364,16 @@ def test_protected_wax_output(self): text = assembly.read_text(encoding='utf-8') self.assertIn('SYSCALL 0', text) self.assertNotIn('HALT', text) + # Alignment padding between globals splits the sparse assembler's + # blocks; --protected fills it so data stays one RW block. + many = self.work / 'many.c' + many.write_text('char a = 1;\nint b = 2;\nchar c[3] = {3};\nlong long d = 4;\nshort e;\n' + 'static int big[300];\nchar f = 5;\n' + 'int main(void) { big[a] = b; return a + b + c[0] + (int)d + e + f + big[1]; }\n', + encoding='utf-8') + image = self.work / 'many.wax' + self.driver(many, '--protected', '--origin', '0x10000', '--stack-top', '0x40000', '-o', image) + self.assertEqual([block['perm'] for block in parse_wax(image.read_bytes())['blocks']], [5, 3]) def test_data_origin_rom_layout(self): sys.path.insert(0, str(ROOT / 'tools/wax'))