diff --git a/CLAUDE.md b/CLAUDE.md index f9dd9c97..a30e60db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,12 +54,25 @@ target/x86_64-pc-windows-msvc/release/nyash.exe ### 🌐 WebAssembly版 ```bash -# ブラウザープレイグラウンド +# WASMビルド方法1: nyash-wasmプロジェクトで直接ビルド +cd projects/nyash-wasm +wasm-pack build --target web + +# WASMビルド方法2: build.shスクリプト使用(古い方法) cd projects/nyash-wasm ./build.sh -# nyash_playground.html をブラウザーで開く + +# 開発サーバー起動(ポート8010推奨) +python3 -m http.server 8010 + +# ブラウザでアクセス +# http://localhost:8010/nyash_playground.html +# http://localhost:8010/enhanced_playground.html +# http://localhost:8010/canvas_playground.html ``` +**注意**: WASMビルドでは一部のBox(TimerBox、AudioBox等)は除外されます。 + ## 📚 ドキュメント構造 ### 🎯 **最重要ドキュメント(開発者向け)** diff --git a/Cargo.toml b/Cargo.toml index 62e5506d..affc35f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,6 +165,19 @@ features = [ "CanvasGradient", "CanvasPattern", "Path2d", + "Performance", + "MouseEvent", + "TouchEvent", + "KeyboardEvent", + "AudioContext", + "AudioContextState", + "AudioBuffer", + "AudioBufferSourceNode", + "GainNode", + "AnalyserNode", + "AudioDestinationNode", + "PeriodicWave", + "OscillatorNode", ] [dev-dependencies] diff --git a/docs/CURRENT_TASK.md b/docs/CURRENT_TASK.md index 477adf76..a0a9d652 100644 --- a/docs/CURRENT_TASK.md +++ b/docs/CURRENT_TASK.md @@ -223,6 +223,40 @@ cargo build --release -j32 --features wasm-backend ## 🎯 今後の優先事項(copilot_issues.txt参照) +### 🌐 **WASMブラウザー版ビルド修正** +- **問題**: projects/nyash-wasmのビルドが失敗(28個のコンパイルエラー) +- **原因と解決策(3ステップ)**: + +#### **Step 1: プラグイン関連の条件コンパイル修正** +- **問題箇所**: + - `src/interpreter/expressions/calls.rs`: `use PluginBoxV2` が無条件 + - `src/bid/loader.rs`: `use libloading` が無条件 +- **修正内容**: + ```rust + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + use crate::runtime::plugin_loader_v2::PluginBoxV2; + ``` + +#### **Step 2: web-sysフィーチャー追加** +- **不足フィーチャー**: + - Performance + - MouseEvent, TouchEvent, KeyboardEvent + - AudioContext, AudioBuffer, GainNode 等 +- **修正内容**: Cargo.tomlの`[dependencies.web-sys]`に追加 + +#### **Step 3: wasm-pack buildコマンド修正** +- **現在**: デフォルトフィーチャー(plugins含む)でビルド +- **修正**: `wasm-pack build --target web --no-default-features --out-dir projects/nyash-wasm/pkg` +- **または**: WASM専用フィーチャー作成 + +- **最終確認**: `wasm-pack build`成功 → `nyash_playground.html`で動作確認 + +### 🚨 **緊急修正: finiシステムの統一** +- **問題**: ビルトインBoxにfiniメソッドがない(設計の不統一) +- **解決**: 全Box型(ビルトイン含む)にfiniメソッド追加 +- **理由**: スコープ離脱時の統一的リソース管理 +- **影響**: StringBox、IntegerBox等16種類のビルトインBox + ### Phase 8.4: AST→MIR Lowering完全実装 - MIR命令セット設計済み(35命令) - Lowering実装開始準備 diff --git a/docs/nyash_core_concepts.md b/docs/nyash_core_concepts.md index 9bbc5833..d3b41ea5 100644 --- a/docs/nyash_core_concepts.md +++ b/docs/nyash_core_concepts.md @@ -231,18 +231,60 @@ Nyashは古典的な継承ではなく、デリゲーション(委譲)モデ ``` - **ファイナライズ (`fini`キーワード):** - - `fini()`は「論理的な解放フック」として機能する特別なメソッドです。 - - インスタンスに対して呼び出されると、そのインスタンスがもはや使用されるべきではないことを示します。 - - クリーンアップ処理を実行し、所有するすべてのフィールドに対して再帰的に`fini()`を呼び出します。 - - ファイナライズされたオブジェクトを使用しようとすると(`fini`の再呼び出しを除く)、実行時エラーが発生します。 + - `fini()`は「リソース解放フック」として機能する特別なメソッドです。 + - **Nyashの明示的哲学**: 自動的な解放は最小限に留め、プログラマーが制御します。 + + **🎯 finiが呼ばれる3つのタイミング:** + + 1. **スコープ離脱時(自動)** + - ローカル変数がスコープを抜ける時に自動的に呼ばれます + - ただし`me`(インスタンス自身)は除外されます + ```nyash + function test() { + local resource = new FileBox("data.txt") + // 関数終了時、resourceのfini()が自動的に呼ばれる + } + ``` + + 2. **明示的呼び出し(推奨)** + - プログラマーが必要に応じて明示的に呼び出します + ```nyash + local file = new FileBox("temp.txt") + file.write("data") + file.fini() // 明示的にリソースを解放 + ``` + + 3. **インスタンスのfini時(カスケード)** + - インスタンスがfiniされる時、そのフィールドもfiniされます + - ただしweakフィールドは除外されます + + **⚠️ 重要な注意点:** + - **フィールド差し替え時にはfiniは呼ばれません**(GC的な「おせっかい」を避ける設計) + - ファイナライズ後のオブジェクト使用は実行時エラーになります + - `fini()`の重複呼び出しは安全(何もしない) + - **すべてのBox型(ビルトイン含む)にfini実装が必要**(設計統一性) + ```nyash box ManagedResource { - init { handle } + init { handle, weak observer } // observerはweakなのでfiniされない + fini() { - // ハンドルを解放したり、他のクリーンアップ処理を実行 + // リソースのクリーンアップ me.console.log("リソースをファイナライズしました。") + // handleは自動的にfiniされる(weakでない限り) } } + + // 使用例 + local res = new ManagedResource() + res.handle = new FileBox("data.txt") + + // ❌ これではfiniは呼ばれない(明示的管理) + res.handle = new FileBox("other.txt") // 古いhandleは解放されない! + + // ✅ 正しい方法 + res.handle.fini() // 明示的に古いリソースを解放 + res.handle = new FileBox("other.txt") ``` ## 3. 標準ライブラリアクセス (using & namespace) 🎉 **Phase 9.75e完了** diff --git a/src/bid/generic_plugin_box.rs b/src/bid/generic_plugin_box.rs index 1d1d32bf..933ad2eb 100644 --- a/src/bid/generic_plugin_box.rs +++ b/src/bid/generic_plugin_box.rs @@ -1,3 +1,6 @@ +#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] +mod plugin_impl { + use crate::bid::{BidError, BidResult, LoadedPlugin}; use crate::bid::tlv::{TlvEncoder, TlvDecoder}; use crate::bid::types::BidTag; @@ -129,4 +132,9 @@ impl fmt::Display for GenericPluginBox { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.fmt_box(f) } -} \ No newline at end of file +} + +} // mod plugin_impl + +#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] +pub use plugin_impl::*; diff --git a/src/bid/loader.rs b/src/bid/loader.rs index 0a4c9eff..05307827 100644 --- a/src/bid/loader.rs +++ b/src/bid/loader.rs @@ -1,4 +1,5 @@ use crate::bid::{BidError, BidResult, NyashHostVtable, NyashPluginInfo, PluginHandle, PLUGIN_ABI_SYMBOL, PLUGIN_INIT_SYMBOL, PLUGIN_INVOKE_SYMBOL, PLUGIN_SHUTDOWN_SYMBOL}; +#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] use libloading::{Library, Symbol}; use std::ffi::c_void; use std::path::{Path, PathBuf}; diff --git a/src/bid/mod.rs b/src/bid/mod.rs index 92fb33e1..4c9a9f42 100644 --- a/src/bid/mod.rs +++ b/src/bid/mod.rs @@ -8,6 +8,7 @@ pub mod metadata; pub mod plugin_api; pub mod bridge; pub mod plugins; +#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] pub mod loader; // pub mod registry; // legacy - v2 plugin system uses BoxFactoryRegistry instead // pub mod plugin_box; // legacy - FileBox専用実装 @@ -19,6 +20,7 @@ pub use error::*; pub use metadata::*; pub use plugin_api::*; pub use bridge::*; +#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] pub use loader::*; // pub use registry::*; // legacy - v2 plugin system uses BoxFactoryRegistry instead // pub use plugin_box::*; // legacy diff --git a/src/boxes/audio_box.rs b/src/boxes/audio_box.rs index bcea374a..f77aed5a 100644 --- a/src/boxes/audio_box.rs +++ b/src/boxes/audio_box.rs @@ -66,38 +66,14 @@ use web_sys::{ #[derive(Debug, Clone)] pub struct AudioBox { base: BoxBase, - #[cfg(target_arch = "wasm32")] - context: Option, - #[cfg(target_arch = "wasm32")] - gain_node: Option, - #[cfg(target_arch = "wasm32")] - analyser_node: Option, volume: f64, is_playing: bool, } impl AudioBox { pub fn new() -> Self { - #[cfg(target_arch = "wasm32")] - let context = AudioContext::new().ok(); - - #[cfg(target_arch = "wasm32")] - let (gain_node, analyser_node) = if let Some(ctx) = &context { - let gain = ctx.create_gain().ok(); - let analyser = ctx.create_analyser().ok(); - (gain, analyser) - } else { - (None, None) - }; - Self { base: BoxBase::new(), - #[cfg(target_arch = "wasm32")] - context, - #[cfg(target_arch = "wasm32")] - gain_node, - #[cfg(target_arch = "wasm32")] - analyser_node, volume: 1.0, is_playing: false, } diff --git a/src/boxes/mod.rs b/src/boxes/mod.rs index 7c2e15a8..c4df1073 100644 --- a/src/boxes/mod.rs +++ b/src/boxes/mod.rs @@ -60,10 +60,16 @@ pub mod math_box; pub mod time_box; pub mod debug_box; pub mod random_box; +// These boxes use web APIs that require special handling in WASM +#[cfg(not(target_arch = "wasm32"))] pub mod timer_box; +#[cfg(not(target_arch = "wasm32"))] pub mod canvas_event_box; +#[cfg(not(target_arch = "wasm32"))] pub mod canvas_loop_box; +#[cfg(not(target_arch = "wasm32"))] pub mod audio_box; +#[cfg(not(target_arch = "wasm32"))] pub mod qr_box; pub mod sound_box; pub mod map_box; @@ -85,10 +91,15 @@ pub use math_box::{MathBox, FloatBox}; pub use time_box::{TimeBox, DateTimeBox}; pub use debug_box::DebugBox; pub use random_box::RandomBox; +#[cfg(not(target_arch = "wasm32"))] pub use timer_box::TimerBox; +#[cfg(not(target_arch = "wasm32"))] pub use canvas_event_box::CanvasEventBox; +#[cfg(not(target_arch = "wasm32"))] pub use canvas_loop_box::CanvasLoopBox; +#[cfg(not(target_arch = "wasm32"))] pub use audio_box::AudioBox; +#[cfg(not(target_arch = "wasm32"))] pub use qr_box::QRBox; pub use sound_box::SoundBox; pub use map_box::MapBox; diff --git a/src/boxes/timer_box.rs b/src/boxes/timer_box.rs index 4291d72a..5846c23d 100644 --- a/src/boxes/timer_box.rs +++ b/src/boxes/timer_box.rs @@ -52,25 +52,18 @@ use std::any::Any; use wasm_bindgen::prelude::*; #[cfg(target_arch = "wasm32")] -use web_sys::{window, Performance}; +use web_sys::window; /// タイマー管理Box #[derive(Debug, Clone)] pub struct TimerBox { base: BoxBase, - #[cfg(target_arch = "wasm32")] - performance: Option, } impl TimerBox { pub fn new() -> Self { - #[cfg(target_arch = "wasm32")] - let performance = window().and_then(|w| w.performance().ok()); - Self { base: BoxBase::new(), - #[cfg(target_arch = "wasm32")] - performance, } } @@ -78,8 +71,12 @@ impl TimerBox { pub fn now(&self) -> f64 { #[cfg(target_arch = "wasm32")] { - if let Some(perf) = &self.performance { - perf.now() + if let Some(window) = window() { + if let Ok(perf) = window.performance() { + perf.now() + } else { + js_sys::Date::now() + } } else { js_sys::Date::now() } diff --git a/src/boxes/web/web_canvas_box.rs b/src/boxes/web/web_canvas_box.rs index dc68e30b..6e217326 100644 --- a/src/boxes/web/web_canvas_box.rs +++ b/src/boxes/web/web_canvas_box.rs @@ -275,6 +275,9 @@ impl BoxCore for WebCanvasBox { write!(f, "WebCanvasBox({}, {}x{})", self.canvas_id, self.width, self.height) } + fn as_any(&self) -> &dyn Any { + self + } fn as_any_mut(&mut self) -> &mut dyn Any { self diff --git a/src/boxes/web/web_console_box.rs b/src/boxes/web/web_console_box.rs index 30505631..ad5393ec 100644 --- a/src/boxes/web/web_console_box.rs +++ b/src/boxes/web/web_console_box.rs @@ -152,6 +152,9 @@ impl BoxCore for WebConsoleBox { write!(f, "WebConsoleBox({})", self.target_element_id) } + fn as_any(&self) -> &dyn Any { + self + } fn as_any_mut(&mut self) -> &mut dyn Any { self diff --git a/src/boxes/web/web_display_box.rs b/src/boxes/web/web_display_box.rs index 7631bd62..c7476ca2 100644 --- a/src/boxes/web/web_display_box.rs +++ b/src/boxes/web/web_display_box.rs @@ -145,6 +145,9 @@ impl BoxCore for WebDisplayBox { write!(f, "WebDisplayBox({})", self.target_element_id) } + fn as_any(&self) -> &dyn Any { + self + } fn as_any_mut(&mut self) -> &mut dyn Any { self diff --git a/src/interpreter/core.rs b/src/interpreter/core.rs index 5941323a..f13aad2f 100644 --- a/src/interpreter/core.rs +++ b/src/interpreter/core.rs @@ -503,6 +503,30 @@ impl NyashInterpreter { } pub(super) fn restore_local_vars(&mut self, saved: HashMap>) { + // 🎯 スコープ離脱時:現在のローカル変数に対してfiniを呼ぶ + // ただし「me」は特別扱い(インスタンス自身なのでfiniしない) + for (name, value) in &self.local_vars { + // 「me」はインスタンス自身なのでスコープ離脱時にfiniしない + if name == "me" { + continue; + } + + // ユーザー定義Box(InstanceBox)の場合 + if let Some(instance) = (**value).as_any().downcast_ref::() { + let _ = instance.fini(); + eprintln!("🔄 Scope exit: Called fini() on local variable '{}' (InstanceBox)", name); + } + // プラグインBoxの場合 + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + if let Some(plugin) = (**value).as_any().downcast_ref::() { + plugin.call_fini(); + eprintln!("🔄 Scope exit: Called fini() on local variable '{}' (PluginBox)", name); + } + // ビルトインBoxは元々finiメソッドを持たないので呼ばない + // (StringBox、IntegerBox等はリソース管理不要) + } + + // その後、保存されていた変数で復元 self.local_vars = saved.into_iter() .map(|(k, v)| (k, Arc::from(v))) // Convert Box to Arc .collect(); @@ -516,6 +540,23 @@ impl NyashInterpreter { } pub(super) fn restore_outbox_vars(&mut self, saved: HashMap>) { + // 🎯 スコープ離脱時:現在のoutbox変数に対してもfiniを呼ぶ + for (name, value) in &self.outbox_vars { + // ユーザー定義Box(InstanceBox)の場合 + if let Some(instance) = (**value).as_any().downcast_ref::() { + let _ = instance.fini(); + eprintln!("🔄 Scope exit: Called fini() on outbox variable '{}' (InstanceBox)", name); + } + // プラグインBoxの場合 + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + if let Some(plugin) = (**value).as_any().downcast_ref::() { + plugin.call_fini(); + eprintln!("🔄 Scope exit: Called fini() on outbox variable '{}' (PluginBox)", name); + } + // ビルトインBoxは元々finiメソッドを持たないので呼ばない(要修正) + } + + // その後、保存されていた変数で復元 self.outbox_vars = saved.into_iter() .map(|(k, v)| (k, Arc::from(v))) // Convert Box to Arc .collect(); diff --git a/src/interpreter/expressions/calls.rs b/src/interpreter/expressions/calls.rs index 1d0b2a73..dae4a783 100644 --- a/src/interpreter/expressions/calls.rs +++ b/src/interpreter/expressions/calls.rs @@ -13,6 +13,7 @@ use crate::instance_v2::InstanceBox; use crate::channel_box::ChannelBox; use crate::interpreter::core::{NyashInterpreter, RuntimeError}; use crate::interpreter::finalization; +#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] use crate::runtime::plugin_loader_v2::PluginBoxV2; use std::sync::Arc; @@ -489,6 +490,7 @@ impl NyashInterpreter { // RangeBox method calls (将来的に追加予定) // PluginBoxV2 method calls + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] if let Some(plugin_box) = obj_value.as_any().downcast_ref::() { return self.execute_plugin_box_v2_method(plugin_box, method, arguments); } @@ -902,6 +904,7 @@ impl NyashInterpreter { } /// Execute method call on PluginBoxV2 + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] fn execute_plugin_box_v2_method( &mut self, plugin_box: &PluginBoxV2, diff --git a/src/interpreter/statements.rs b/src/interpreter/statements.rs index e58cedcf..0a65349b 100644 --- a/src/interpreter/statements.rs +++ b/src/interpreter/statements.rs @@ -305,17 +305,8 @@ impl NyashInterpreter { } } - // 既存のフィールド値があれば fini() を呼ぶ - if let Some(old_field_value) = instance.get_field(field) { - if let Some(old_instance) = (*old_field_value).as_any().downcast_ref::() { - let _ = old_instance.fini(); - finalization::mark_as_finalized(old_instance.box_id()); - } - #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] - if let Some(old_plugin) = (*old_field_value).as_any().downcast_ref::() { - old_plugin.call_fini(); - } - } + // 🚨 フィールド差し替え時の自動finiは削除(Nyashの明示的哲学) + // プログラマーが必要なら明示的にfini()を呼ぶべき instance.set_field(field, Arc::from(val.clone_box())) .map_err(|e| RuntimeError::InvalidOperation { message: e })?; @@ -342,17 +333,8 @@ impl NyashInterpreter { }); } - // 既存のthis.field値があれば fini() を呼ぶ - if let Some(old_field_value) = instance.get_field(field) { - if let Some(old_instance) = (*old_field_value).as_any().downcast_ref::() { - let _ = old_instance.fini(); - finalization::mark_as_finalized(old_instance.box_id()); - } - #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] - if let Some(old_plugin) = (*old_field_value).as_any().downcast_ref::() { - old_plugin.call_fini(); - } - } + // 🚨 フィールド差し替え時の自動finiは削除(Nyashの明示的哲学) + // プログラマーが必要なら明示的にfini()を呼ぶべき instance.set_field(field, Arc::from(val.clone_box())) .map_err(|e| RuntimeError::InvalidOperation { message: e })?; @@ -379,17 +361,8 @@ impl NyashInterpreter { }); } - // 既存のme.field値があれば fini() を呼ぶ - if let Some(old_field_value) = instance.get_field(field) { - if let Some(old_instance) = (*old_field_value).as_any().downcast_ref::() { - let _ = old_instance.fini(); - finalization::mark_as_finalized(old_instance.box_id()); - } - #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] - if let Some(old_plugin) = (*old_field_value).as_any().downcast_ref::() { - old_plugin.call_fini(); - } - } + // 🚨 フィールド差し替え時の自動finiは削除(Nyashの明示的哲学) + // プログラマーが必要なら明示的にfini()を呼ぶべき instance.set_field(field, Arc::from(val.clone_box())) .map_err(|e| RuntimeError::InvalidOperation { message: e })?; diff --git a/src/interpreter/web_methods.rs b/src/interpreter/web_methods.rs index f0d105e0..964a4529 100644 --- a/src/interpreter/web_methods.rs +++ b/src/interpreter/web_methods.rs @@ -15,6 +15,8 @@ use super::*; #[cfg(target_arch = "wasm32")] use crate::boxes::web::{WebDisplayBox, WebConsoleBox, WebCanvasBox}; +#[cfg(target_arch = "wasm32")] +use crate::boxes::FloatBox; #[cfg(target_arch = "wasm32")] impl NyashInterpreter { diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index 1ddee45f..bf5929c2 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -399,8 +399,14 @@ mod stub { use once_cell::sync::Lazy; use std::sync::{Arc, RwLock}; - pub struct PluginLoaderV2; - impl PluginLoaderV2 { pub fn new() -> Self { Self } } + pub struct PluginLoaderV2 { + pub config: Option<()>, // Dummy config for compatibility + } + impl PluginLoaderV2 { + pub fn new() -> Self { + Self { config: None } + } + } impl PluginLoaderV2 { pub fn load_config(&mut self, _p: &str) -> BidResult<()> { Ok(()) } pub fn load_all_plugins(&self) -> BidResult<()> { Ok(()) }