📚 docs: Record field declaration design discussion in papers

## Summary
Documented the "init block vs fields-at-top" design discussion as a valuable example of AI-human collaboration in language design.

## Changes

### Paper G (AI Collaboration)
- Added field-declaration-design.md documenting the entire discussion flow
- Showcased how complex init block proposal evolved to simple "fields at top" rule
- Demonstrates AI's tendency toward complexity vs human intuition for simplicity

### Paper H (AI Practical Patterns)
- Added Pattern #17: "Gradual Refinement Pattern" (段階的洗練型)
- Documents the process: Complex AI proposal → Detailed analysis → Human insight → Convergence
- Field declaration design as a typical example

### Paper K (Explosive Incidents)
- Added Incident #046: "init block vs fields-at-top incident"
- Updated total count to 46 incidents
- Shows how a single human comment redirected entire design approach

## Design Decision
After analysis, decided that BoxIndex should remain a compiler-internal structure, not a core Box:
- Core Boxes: User-instantiable runtime values (String, Integer, Array, Map)
- Compiler internals: BoxIndex for name resolution (compile-time only)
- Clear separation of concerns between language features and compiler tools

## Philosophy
This discussion exemplifies key principles:
- The best design needs no explanation
- Constraints provide clarity, not limitation
- "Everything is Box" doesn't mean "compiler internals are Boxes"
- AI tends toward theoretical completeness; humans toward practical simplicity

🐱 Sometimes the simplest answer is right in front of us\!
This commit is contained in:
Selfhosting Dev
2025-09-16 14:57:05 +09:00
parent 6ca56b0652
commit 47f4ca0e44
14 changed files with 804 additions and 96 deletions

134
src/runner/box_index.rs Normal file
View File

@ -0,0 +1,134 @@
/*!
* BoxIndex — minimal view over aliases and plugin box types
*
* Purpose: allow using/namespace resolver to make decisions that depend
* on plugin-visible type names (e.g., enforcing strict prefix rules) and
* to surface aliases defined in nyash.toml/env.
*/
use std::collections::{HashMap, HashSet};
use once_cell::sync::Lazy;
use std::sync::RwLock;
#[derive(Clone, Default)]
pub struct BoxIndex {
pub aliases: HashMap<String, String>,
pub plugin_boxes: HashSet<String>,
pub plugin_meta: HashMap<String, PluginMeta>,
pub plugins_require_prefix_global: bool,
}
impl BoxIndex {
pub fn build_current() -> Self {
// aliases from nyash.toml and env
let mut aliases: HashMap<String, String> = HashMap::new();
if let Ok(text) = std::fs::read_to_string("nyash.toml") {
if let Ok(doc) = toml::from_str::<toml::Value>(&text) {
if let Some(alias_tbl) = doc.get("aliases").and_then(|v| v.as_table()) {
for (k, v) in alias_tbl.iter() {
if let Some(target) = v.as_str() { aliases.insert(k.to_string(), target.to_string()); }
}
}
}
}
if let Ok(raw) = std::env::var("NYASH_ALIASES") {
for ent in raw.split(',') {
if let Some((k,v)) = ent.split_once('=') {
let k = k.trim(); let v = v.trim();
if !k.is_empty() && !v.is_empty() { aliases.insert(k.to_string(), v.to_string()); }
}
}
}
// plugin box types (best-effort; may be empty if host not initialized yet)
let mut plugin_boxes: HashSet<String> = HashSet::new();
let mut plugin_meta: HashMap<String, PluginMeta> = HashMap::new();
let mut plugins_require_prefix_global = false;
// Read per-plugin meta and global flags from nyash.toml when available
if let Ok(text) = std::fs::read_to_string("nyash.toml") {
if let Ok(doc) = toml::from_str::<toml::Value>(&text) {
if let Some(plugins_tbl) = doc.get("plugins").and_then(|v| v.as_table()) {
// Global switch: [plugins].require_prefix = true
if let Some(v) = plugins_tbl.get("require_prefix").and_then(|v| v.as_bool()) {
plugins_require_prefix_global = v;
}
for (k, v) in plugins_tbl.iter() {
// Skip non-table entries (string entries are plugin roots)
if let Some(t) = v.as_table() {
let prefix = t.get("prefix").and_then(|x| x.as_str()).map(|s| s.to_string());
let require_prefix = t.get("require_prefix").and_then(|x| x.as_bool()).unwrap_or(false);
let expose_short_names = t.get("expose_short_names").and_then(|x| x.as_bool()).unwrap_or(true);
plugin_meta.insert(k.clone(), PluginMeta { prefix, require_prefix, expose_short_names });
}
}
}
}
}
let host = crate::runtime::get_global_plugin_host();
if let Ok(h) = host.read() {
if let Some(cfg) = h.config_ref() {
for (_lib, def) in &cfg.libraries {
for bt in &def.boxes { plugin_boxes.insert(bt.clone()); }
}
}
}
Self { aliases, plugin_boxes, plugin_meta, plugins_require_prefix_global }
}
pub fn is_known_plugin_short(name: &str) -> bool {
// Prefer global index view
if GLOBAL.read().ok().map(|g| g.plugin_boxes.contains(name)).unwrap_or(false) {
return true;
}
// Env override list
if let Ok(raw) = std::env::var("NYASH_KNOWN_PLUGIN_SHORTNAMES") {
let set: HashSet<String> = raw.split(',').map(|s| s.trim().to_string()).collect();
if set.contains(name) { return true; }
}
// Minimal fallback set
const KNOWN: &[&str] = &[
"ArrayBox","MapBox","StringBox","ConsoleBox","FileBox","PathBox","MathBox","IntegerBox","TOMLBox"
];
KNOWN.iter().any(|k| *k == name)
}
}
// Global BoxIndex view (rebuilt on-demand)
static GLOBAL: Lazy<RwLock<BoxIndex>> = Lazy::new(|| RwLock::new(BoxIndex::default()));
// Global resolve cache (keyed by tgt|base|strict|paths)
static RESOLVE_CACHE: Lazy<RwLock<HashMap<String, String>>> = Lazy::new(|| RwLock::new(HashMap::new()));
pub fn refresh_box_index() {
let next = BoxIndex::build_current();
if let Ok(mut w) = GLOBAL.write() { *w = next; }
}
pub fn get_box_index() -> BoxIndex {
GLOBAL.read().ok().map(|g| g.clone()).unwrap_or_default()
}
pub fn cache_get(key: &str) -> Option<String> {
RESOLVE_CACHE.read().ok().and_then(|m| m.get(key).cloned())
}
pub fn cache_put(key: &str, value: String) {
if let Ok(mut m) = RESOLVE_CACHE.write() { m.insert(key.to_string(), value); }
}
pub fn cache_clear() {
if let Ok(mut m) = RESOLVE_CACHE.write() { m.clear(); }
}
#[derive(Clone, Debug, Default)]
pub struct PluginMeta {
pub prefix: Option<String>,
pub require_prefix: bool,
pub expose_short_names: bool,
}
pub fn get_plugin_meta(plugin: &str) -> Option<PluginMeta> {
GLOBAL.read().ok().and_then(|g| g.plugin_meta.get(plugin).cloned())
}

View File

@ -21,6 +21,7 @@ mod json_v0_bridge;
mod mir_json_emit;
mod pipe_io;
mod pipeline;
mod box_index;
mod tasks;
mod build;
mod dispatch;
@ -61,7 +62,26 @@ impl NyashRunner {
}
// Using/module overrides pre-processing
let mut using_ctx = self.init_using_context();
let pending_using: Vec<(String, Option<String>)> = Vec::new();
let mut pending_using: Vec<(String, Option<String>)> = Vec::new();
// CLI --using SPEC entries (SPEC: 'ns', 'ns as Alias', '"path" as Alias')
for spec in &self.config.cli_usings {
let s = spec.trim();
if s.is_empty() { continue; }
let (target, alias) = if let Some(pos) = s.find(" as ") {
(s[..pos].trim().to_string(), Some(s[pos+4..].trim().to_string()))
} else { (s.to_string(), None) };
// Normalize quotes for path
let is_path = target.starts_with('"') || target.starts_with("./") || target.starts_with('/') || target.ends_with(".nyash");
if is_path {
let path = target.trim_matches('"').to_string();
let name = alias.clone().unwrap_or_else(|| {
std::path::Path::new(&path).file_stem().and_then(|s| s.to_str()).unwrap_or("module").to_string()
});
pending_using.push((name, Some(path)));
} else {
pending_using.push((target, alias));
}
}
for (ns, path) in using_ctx.pending_modules.iter() {
let sb = crate::box_trait::StringBox::new(path.clone());
crate::runtime::modules_registry::set(ns.clone(), Box::new(sb));
@ -135,6 +155,13 @@ impl NyashRunner {
}
}
// Lint: fields must be at top of box
let strict_fields = std::env::var("NYASH_FIELDS_TOP_STRICT").ok().as_deref() == Some("1");
if let Err(e) = pipeline::lint_fields_top(&code, strict_fields, self.config.cli_verbose) {
eprintln!("❌ Lint error: {}", e);
std::process::exit(1);
}
// Env overrides for using rules
// Merge late env overrides (if any)
if let Ok(paths) = std::env::var("NYASH_USING_PATH") {
@ -159,7 +186,7 @@ impl NyashRunner {
let verbose = std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1");
let ctx = std::path::Path::new(filename).parent();
for (ns, alias) in pending_using.iter() {
let value = match resolve_using_target(ns, false, &using_ctx.pending_modules, &using_ctx.using_paths, ctx, strict, verbose) {
let value = match resolve_using_target(ns, false, &using_ctx.pending_modules, &using_ctx.using_paths, &using_ctx.aliases, ctx, strict, verbose) {
Ok(v) => v,
Err(e) => { eprintln!("❌ using: {}", e); std::process::exit(1); }
};
@ -184,13 +211,15 @@ impl NyashRunner {
}
}
// 🏭 Phase 9.78b: Initialize unified registry
runtime::init_global_unified_registry();
// Try to initialize BID plugins from nyash.toml (best-effort)
// 🏭 Phase 9.78b: Initialize unified registry
runtime::init_global_unified_registry();
// Try to initialize BID plugins from nyash.toml (best-effort)
// Allow disabling during snapshot/CI via NYASH_DISABLE_PLUGINS=1
if std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() != Some("1") {
runner_plugin_init::init_bid_plugins();
// Build BoxIndex after plugin host is initialized
crate::runner::box_index::refresh_box_index();
}
// Allow interpreter to create plugin-backed boxes via unified registry
// Opt-in by default for FileBox/TOMLBox which are required by ny-config and similar tools.

View File

@ -8,7 +8,7 @@ use std::io::Read;
use std::process::Stdio;
use std::time::{Duration, Instant};
use std::thread::sleep;
use crate::runner::pipeline::suggest_in_base;
use crate::runner::pipeline::{suggest_in_base, resolve_using_target};
// (moved) suggest_in_base is now in runner/pipeline.rs
@ -610,30 +610,26 @@ impl NyashRunner {
cleaned_code_owned = out;
code_ref = &cleaned_code_owned;
// Register modules into minimal registry with best-effort path resolution
// Register modules with resolver (aliases/modules/paths)
let using_ctx = self.init_using_context();
let strict = std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1");
let verbose = std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1");
let ctx_dir = std::path::Path::new(filename).parent();
for (ns_or_alias, alias_or_path) in used_names {
// alias_or_path Some(path) means this entry was a direct path using
if let Some(path) = alias_or_path {
let sb = crate::box_trait::StringBox::new(path);
crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
} else {
let rel = format!("apps/{}.nyash", ns_or_alias.replace('.', "/"));
let exists = std::path::Path::new(&rel).exists();
if !exists && std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
eprintln!("[using] unresolved namespace '{}'; tried '{}'. Hint: add @module {}={} or --module {}={}", ns_or_alias, rel, ns_or_alias, rel, ns_or_alias, rel);
// naive candidates by suffix within common bases
let leaf = ns_or_alias.split('.').last().unwrap_or(&ns_or_alias);
let mut cands: Vec<String> = Vec::new();
suggest_in_base("apps", leaf, &mut cands);
if cands.len() < 5 { suggest_in_base("lib", leaf, &mut cands); }
if cands.len() < 5 { suggest_in_base(".", leaf, &mut cands); }
if !cands.is_empty() {
eprintln!("[using] candidates: {}", cands.join(", "));
match resolve_using_target(&ns_or_alias, false, &using_ctx.pending_modules, &using_ctx.using_paths, &using_ctx.aliases, ctx_dir, strict, verbose) {
Ok(value) => {
let sb = crate::box_trait::StringBox::new(value);
crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
}
Err(e) => {
eprintln!("❌ using: {}", e);
std::process::exit(1);
}
}
let path_or_ns = if exists { rel } else { ns_or_alias.clone() };
let sb = crate::box_trait::StringBox::new(path_or_ns);
crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
}
}
}

View File

@ -1,5 +1,7 @@
use crate::parser::NyashParser;
use crate::interpreter::NyashInterpreter;
use crate::runner_plugin_init;
use crate::runner::pipeline::{resolve_using_target, UsingContext};
use std::{fs, process};
/// Execute Nyash file with interpreter
@ -12,7 +14,12 @@ pub fn execute_nyash_file(filename: &str, debug_fuel: Option<usize>) {
process::exit(1);
}
};
// Initialize plugin host and mappings (idempotent)
if std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() != Some("1") {
runner_plugin_init::init_bid_plugins();
crate::runner::box_index::refresh_box_index();
}
println!("📝 File contents:\n{}", code);
println!("\n🚀 Parsing and executing...\n");
@ -20,9 +27,60 @@ pub fn execute_nyash_file(filename: &str, debug_fuel: Option<usize>) {
std::fs::create_dir_all("development/debug_hang_issue").ok();
std::fs::write("development/debug_hang_issue/test.txt", "START").ok();
// Optional: using pre-processing (strip lines and register modules)
let enable_using = std::env::var("NYASH_ENABLE_USING").ok().as_deref() == Some("1");
let mut code_ref: std::borrow::Cow<'_, str> = std::borrow::Cow::Borrowed(&code);
if enable_using {
let mut out = String::with_capacity(code.len());
let mut used_names: Vec<(String, Option<String>)> = Vec::new();
for line in code.lines() {
let t = line.trim_start();
if t.starts_with("using ") {
let rest0 = t.strip_prefix("using ").unwrap().trim();
let rest0 = rest0.strip_suffix(';').unwrap_or(rest0).trim();
let (target, alias) = if let Some(pos) = rest0.find(" as ") {
(rest0[..pos].trim().to_string(), Some(rest0[pos+4..].trim().to_string()))
} else { (rest0.to_string(), None) };
let is_path = target.starts_with('"') || target.starts_with("./") || target.starts_with('/') || target.ends_with(".nyash");
if is_path {
let path = target.trim_matches('"').to_string();
let name = alias.clone().unwrap_or_else(|| {
std::path::Path::new(&path).file_stem().and_then(|s| s.to_str()).unwrap_or("module").to_string()
});
used_names.push((name, Some(path)));
} else {
used_names.push((target, alias));
}
continue;
}
out.push_str(line);
out.push('\n');
}
// Resolve and register
let using_ctx = UsingContext { using_paths: vec!["apps".into(), "lib".into(), ".".into()], pending_modules: vec![], aliases: std::collections::HashMap::new() };
let strict = std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1");
let verbose = std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1");
let ctx_dir = std::path::Path::new(filename).parent();
for (ns_or_alias, alias_or_path) in used_names {
if let Some(path) = alias_or_path {
let sb = crate::box_trait::StringBox::new(path);
crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
} else {
match resolve_using_target(&ns_or_alias, false, &using_ctx.pending_modules, &using_ctx.using_paths, &using_ctx.aliases, ctx_dir, strict, verbose) {
Ok(value) => {
let sb = crate::box_trait::StringBox::new(value);
crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
}
Err(e) => { eprintln!("❌ using: {}", e); std::process::exit(1); }
}
}
}
code_ref = std::borrow::Cow::Owned(out);
}
// Parse the code with debug fuel limit
eprintln!("🔍 DEBUG: Starting parse with fuel: {:?}...", debug_fuel);
let ast = match NyashParser::parse_from_string_with_fuel(&code, debug_fuel) {
let ast = match NyashParser::parse_from_string_with_fuel(&*code_ref, debug_fuel) {
Ok(ast) => {
eprintln!("🔍 DEBUG: Parse completed, AST created");
ast

View File

@ -8,11 +8,14 @@
*/
use super::*;
use super::box_index::BoxIndex;
use std::collections::HashMap;
/// Using/module resolution context accumulated from config/env/nyash.toml
pub(super) struct UsingContext {
pub using_paths: Vec<String>,
pub pending_modules: Vec<(String, String)>,
pub aliases: std::collections::HashMap<String, String>,
}
impl NyashRunner {
@ -20,6 +23,7 @@ impl NyashRunner {
pub(super) fn init_using_context(&self) -> UsingContext {
let mut using_paths: Vec<String> = Vec::new();
let mut pending_modules: Vec<(String, String)> = Vec::new();
let mut aliases: std::collections::HashMap<String, String> = std::collections::HashMap::new();
// Defaults
using_paths.extend(["apps", "lib", "."].into_iter().map(|s| s.to_string()));
@ -45,6 +49,14 @@ impl NyashRunner {
}
}
}
// Optional: [aliases] table maps short name -> path or namespace token
if let Some(alias_tbl) = doc.get("aliases").and_then(|v| v.as_table()) {
for (k, v) in alias_tbl.iter() {
if let Some(target) = v.as_str() {
aliases.insert(k.to_string(), target.to_string());
}
}
}
}
}
}
@ -67,8 +79,17 @@ impl NyashRunner {
if !s.is_empty() { using_paths.push(s.to_string()); }
}
}
// Env aliases: comma-separated k=v pairs
if let Ok(raw) = std::env::var("NYASH_ALIASES") {
for ent in raw.split(',') {
if let Some((k,v)) = ent.split_once('=') {
let k = k.trim(); let v = v.trim();
if !k.is_empty() && !v.is_empty() { aliases.insert(k.to_string(), v.to_string()); }
}
}
}
UsingContext { using_paths, pending_modules }
UsingContext { using_paths, pending_modules, aliases }
}
}
@ -107,13 +128,81 @@ pub(super) fn resolve_using_target(
is_path: bool,
modules: &[(String, String)],
using_paths: &[String],
aliases: &HashMap<String, String>,
context_dir: Option<&std::path::Path>,
strict: bool,
verbose: bool,
) -> Result<String, String> {
if is_path { return Ok(tgt.to_string()); }
let trace = verbose || std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1");
// Strict plugin prefix: if enabled and target matches a known plugin box type
// and is not qualified (contains '.'), require a qualified/prefixed name.
// Strict mode: env or nyash.toml [plugins] require_prefix=true
let mut strict_effective = strict;
if !strict_effective {
if let Ok(text) = std::fs::read_to_string("nyash.toml") {
if let Ok(doc) = toml::from_str::<toml::Value>(&text) {
if let Some(tbl) = doc.get("plugins").and_then(|v| v.as_table()) {
if let Some(v) = tbl.get("require_prefix").and_then(|v| v.as_bool()) { if v { strict_effective = true; } }
}
}
}
}
if std::env::var("NYASH_PLUGIN_REQUIRE_PREFIX").ok().as_deref() == Some("1") { strict_effective = true; }
if strict_effective {
let mut is_plugin_short = super::box_index::BoxIndex::is_known_plugin_short(tgt);
if !is_plugin_short {
// Fallback: heuristic list or env override
if let Ok(raw) = std::env::var("NYASH_KNOWN_PLUGIN_SHORTNAMES") {
let set: std::collections::HashSet<String> = raw.split(',').map(|s| s.trim().to_string()).collect();
is_plugin_short = set.contains(tgt);
} else {
// Minimal builtins set
const KNOWN: &[&str] = &[
"ArrayBox","MapBox","StringBox","ConsoleBox","FileBox","PathBox","MathBox","IntegerBox","TOMLBox"
];
is_plugin_short = KNOWN.iter().any(|k| *k == tgt);
}
}
if is_plugin_short && !tgt.contains('.') {
return Err(format!("plugin short name '{}' requires prefix (strict)", tgt));
}
}
let key = {
let base = context_dir.and_then(|p| p.to_str()).unwrap_or("");
format!("{}|{}|{}|{}", tgt, base, strict as i32, using_paths.join(":"))
};
if let Some(hit) = crate::runner::box_index::cache_get(&key) {
if trace { eprintln!("[using/cache] '{}' -> '{}'", tgt, hit); }
return Ok(hit);
}
// Resolve aliases early (provided map)
if let Some(v) = aliases.get(tgt) {
if trace { eprintln!("[using/resolve] alias '{}' -> '{}'", tgt, v); }
crate::runner::box_index::cache_put(&key, v.clone());
return Ok(v.clone());
}
// Also consult env aliases
if let Ok(raw) = std::env::var("NYASH_ALIASES") {
for ent in raw.split(',') {
if let Some((k,v)) = ent.split_once('=') {
if k.trim() == tgt {
let out = v.trim().to_string();
if trace { eprintln!("[using/resolve] env-alias '{}' -> '{}'", tgt, out); }
crate::runner::box_index::cache_put(&key, out.clone());
return Ok(out);
}
}
}
}
// 1) modules mapping
if let Some((_, p)) = modules.iter().find(|(n, _)| n == tgt) { return Ok(p.clone()); }
if let Some((_, p)) = modules.iter().find(|(n, _)| n == tgt) {
let out = p.clone();
if trace { eprintln!("[using/resolve] modules '{}' -> '{}'", tgt, out); }
crate::runner::box_index::cache_put(&key, out.clone());
return Ok(out);
}
// 2) build candidate list: relative then using-paths
let rel = tgt.replace('.', "/") + ".nyash";
let mut cand: Vec<String> = Vec::new();
@ -123,11 +212,123 @@ pub(super) fn resolve_using_target(
if c.exists() { cand.push(c.to_string_lossy().to_string()); }
}
if cand.is_empty() {
if verbose { eprintln!("[using] unresolved '{}' (searched: rel+paths)", tgt); }
if trace {
// Try suggest candidates by leaf across bases (apps/lib/.)
let leaf = tgt.split('.').last().unwrap_or(tgt);
let mut cands: Vec<String> = Vec::new();
suggest_in_base("apps", leaf, &mut cands);
if cands.len() < 5 { suggest_in_base("lib", leaf, &mut cands); }
if cands.len() < 5 { suggest_in_base(".", leaf, &mut cands); }
if cands.is_empty() {
eprintln!("[using] unresolved '{}' (searched: rel+paths)", tgt);
} else {
eprintln!("[using] unresolved '{}' (searched: rel+paths) candidates: {}", tgt, cands.join(", "));
}
}
return Ok(tgt.to_string());
}
if cand.len() > 1 && strict {
return Err(format!("ambiguous using '{}': {}", tgt, cand.join(", ")));
}
Ok(cand.remove(0))
let out = cand.remove(0);
if trace { eprintln!("[using/resolve] '{}' -> '{}'", tgt, out); }
crate::runner::box_index::cache_put(&key, out.clone());
Ok(out)
}
/// Lint: enforce "fields must be at the top of box" rule.
/// - Warns by default (when verbose); when `strict` is true, returns Err on any violation.
pub(super) fn lint_fields_top(code: &str, strict: bool, verbose: bool) -> Result<(), String> {
let mut brace: i32 = 0;
let mut in_box = false;
let mut box_depth: i32 = 0;
let mut seen_method = false;
let mut cur_box: String = String::new();
let mut violations: Vec<(usize, String, String)> = Vec::new(); // (line, field, box)
for (idx, line) in code.lines().enumerate() {
let lno = idx + 1;
let pre_brace = brace;
let trimmed = line.trim();
// Count braces for this line
let opens = line.matches('{').count() as i32;
let closes = line.matches('}').count() as i32;
// Enter box on same-line K&R style: `box Name {` or `static box Name {`
if !in_box && trimmed.starts_with("box ") || trimmed.starts_with("static box ") {
// capture name
let mut name = String::new();
let after = if let Some(rest) = trimmed.strip_prefix("static box ") { rest } else { trimmed.strip_prefix("box ").unwrap_or("") };
for ch in after.chars() {
if ch.is_alphanumeric() || ch == '_' { name.push(ch); } else { break; }
}
// require K&R brace on same line to start tracking
if opens > 0 {
in_box = true;
cur_box = name;
box_depth = pre_brace + 1; // assume one level for box body
seen_method = false;
}
}
if in_box {
// Top-level inside box only
if pre_brace == box_depth {
// Skip empty/comment lines
if !trimmed.is_empty() && !trimmed.starts_with("//") {
// Detect method: name(args) {
let is_method = {
// starts with identifier then '(' and later '{'
let mut it = trimmed.chars();
let mut ident = String::new();
while let Some(c) = it.next() { if c.is_whitespace() { continue; } if c.is_alphabetic() || c=='_' { ident.push(c); break; } else { break; } }
while let Some(c) = it.next() { if c.is_alphanumeric() || c=='_' { ident.push(c); } else { break; } }
trimmed.contains('(') && trimmed.ends_with('{') && !ident.is_empty()
};
if is_method { seen_method = true; }
// Detect field: ident ':' Type (rough heuristic)
let is_field = {
let parts: Vec<&str> = trimmed.split(':').collect();
if parts.len() == 2 {
let lhs = parts[0].trim();
let rhs = parts[1].trim();
let lhs_ok = !lhs.is_empty() && lhs.chars().next().map(|c| c.is_alphabetic() || c=='_').unwrap_or(false);
let rhs_ok = !rhs.is_empty() && rhs.chars().next().map(|c| c.is_alphabetic() || c=='_').unwrap_or(false);
lhs_ok && rhs_ok && !trimmed.contains('(') && !trimmed.contains(')')
} else { false }
};
if is_field && seen_method {
violations.push((lno, trimmed.to_string(), cur_box.clone()));
}
}
}
// Exit box when closing brace reduces depth below box_depth
let post_brace = pre_brace + opens - closes;
if post_brace < box_depth { in_box = false; cur_box.clear(); }
}
// Update brace after processing
brace += opens - closes;
}
if violations.is_empty() {
return Ok(());
}
if strict {
// Compose error message
let mut msg = String::from("Field declarations must appear at the top of box. Violations:\n");
for (lno, fld, bx) in violations.iter().take(10) {
msg.push_str(&format!(" line {} in box {}: '{}" , lno, if bx.is_empty(){"<unknown>"} else {bx}, fld));
msg.push_str("'\n");
}
if violations.len() > 10 { msg.push_str(&format!(" ... and {} more\n", violations.len()-10)); }
return Err(msg);
}
if verbose || std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1") {
for (lno, fld, bx) in violations {
eprintln!("[lint] fields-top: line {} in box {} -> {}", lno, if bx.is_empty(){"<unknown>"} else {&bx}, fld);
}
}
Ok(())
}

View File

@ -91,6 +91,57 @@ impl NyashRunner {
Err(e) => { eprintln!("[ny-compiler] open tmp failed: {}", e); return false; }
}
}
// Python MVP-first: prefer the lightweight harness to produce JSON v0
if let Ok(py3) = which::which("python3") {
let py = std::path::Path::new("tools/ny_parser_mvp.py");
if py.exists() {
let mut cmd = std::process::Command::new(&py3);
cmd.arg(py).arg(&tmp_path);
let out = match cmd.output() { Ok(o) => o, Err(e) => { eprintln!("[ny-compiler] python harness failed to spawn: {}", e); return false; } };
if out.status.success() {
if let Ok(line) = String::from_utf8(out.stdout).map(|s| s.lines().next().unwrap_or("").to_string()) {
if line.contains("\"version\"") && line.contains("\"kind\"") {
match super::json_v0_bridge::parse_json_v0_to_module(&line) {
Ok(module) => {
super::json_v0_bridge::maybe_dump_mir(&module);
let emit_only = std::env::var("NYASH_NY_COMPILER_EMIT_ONLY").unwrap_or_else(|_| "1".to_string()) == "1";
if emit_only { return false; }
// Prefer PyVM for selfhost pipeline (parity reference)
if std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1") {
// Reuse the common PyVM runner path
let tmp_dir = std::path::Path::new("tmp");
let _ = std::fs::create_dir_all(tmp_dir);
let mir_json_path = tmp_dir.join("nyash_pyvm_mir.json");
if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(&module, &mir_json_path) {
eprintln!("❌ PyVM MIR JSON emit error: {}", e);
process::exit(1);
}
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
eprintln!("[Bridge] using PyVM (selfhost-py) → {}", mir_json_path.display());
}
let entry = if module.functions.contains_key("Main.main") { "Main.main" } else if module.functions.contains_key("main") { "main" } else { "Main.main" };
let status = std::process::Command::new(&py3)
.args(["tools/pyvm_runner.py", "--in", &mir_json_path.display().to_string(), "--entry", entry])
.status().map_err(|e| format!("spawn pyvm: {}", e)).unwrap();
let code = status.code().unwrap_or(1);
if !status.success() {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
eprintln!("❌ PyVM (selfhost-py) failed (status={})", code);
}
}
println!("Result: {}", code);
std::process::exit(code);
}
self.execute_mir_module(&module);
return true;
}
Err(e) => { eprintln!("[ny-compiler] json parse error: {}", e); return false; }
}
}
}
}
}
}
// EXE-first: if requested, try external parser EXE (nyash_compiler)
if std::env::var("NYASH_USE_NY_COMPILER_EXE").ok().as_deref() == Some("1") {
// Resolve parser EXE path
@ -204,57 +255,60 @@ impl NyashRunner {
}
}
// Fallback: run compiler.nyash via VM(PyVM) and pick the JSON line
// Guard against recursion: ensure child does NOT enable selfhost pipeline.
// Fallback: inline VM run (embed source into a tiny wrapper that prints JSON)
// This avoids CLI arg forwarding complexity and does not require FileBox.
let mut raw = String::new();
{
// Locate current nyash executable
// Escape source for embedding as string literal
let mut esc = String::with_capacity(code_ref.len());
for ch in code_ref.chars() {
match ch {
'\\' => esc.push_str("\\\\"),
'"' => esc.push_str("\\\""),
'\n' => esc.push_str("\n"),
'\r' => esc.push_str(""),
_ => esc.push(ch),
}
}
let inline_path = std::path::Path::new("tmp").join("inline_selfhost_emit.nyash");
let inline_code = format!(
"include \"apps/selfhost-compiler/boxes/parser_box.nyash\"\ninclude \"apps/selfhost-compiler/boxes/emitter_box.nyash\"\nstatic box Main {{\n main(args) {{\n local s = \"{}\"\n local p = new ParserBox()\n local json = p.parse_program2(s)\n local e = new EmitterBox()\n json = e.emit_program(json, \"[]\")\n print(json)\n return 0\n }}\n}}\n",
esc
);
if let Err(e) = std::fs::write(&inline_path, inline_code) {
eprintln!("[ny-compiler] write inline failed: {}", e);
return false;
}
let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("target/release/nyash"));
let mut cmd = std::process::Command::new(exe);
cmd.arg("--backend").arg("vm").arg("apps/selfhost-compiler/compiler.nyash");
// Pass script args to child when gated
if std::env::var("NYASH_NY_COMPILER_MIN_JSON").ok().as_deref() == Some("1") { cmd.arg("--").arg("--min-json"); }
if std::env::var("NYASH_SELFHOST_READ_TMP").ok().as_deref() == Some("1") { cmd.arg("--").arg("--read-tmp"); }
// Recursion guard and minimal, quiet env for child
cmd.arg("--backend").arg("vm").arg(&inline_path);
cmd.env_remove("NYASH_USE_NY_COMPILER");
cmd.env_remove("NYASH_CLI_VERBOSE");
cmd.env("NYASH_JSON_ONLY", "1");
if let Ok(v) = std::env::var("NYASH_JSON_INCLUDE_USINGS") { cmd.env("NYASH_JSON_INCLUDE_USINGS", v); }
if let Ok(v) = std::env::var("NYASH_ENABLE_USING") { cmd.env("NYASH_ENABLE_USING", v); }
// Timeout guard (default 2000ms)
let timeout_ms: u64 = std::env::var("NYASH_NY_COMPILER_TIMEOUT_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(2000);
let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => { eprintln!("[ny-compiler] spawn nyash vm failed: {}", e); return false; }
};
let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { eprintln!("[ny-compiler] spawn inline vm failed: {}", e); return false; } };
let mut ch_stdout = child.stdout.take();
let mut ch_stderr = child.stderr.take();
let start = Instant::now();
let mut timed_out = false;
loop {
match child.try_wait() {
Ok(Some(_status)) => { break; }
Ok(Some(_)) => break,
Ok(None) => {
if start.elapsed() >= Duration::from_millis(timeout_ms) {
let _ = child.kill();
let _ = child.wait();
timed_out = true;
break;
let _ = child.kill(); let _ = child.wait(); timed_out = true; break;
}
sleep(Duration::from_millis(10));
}
Err(e) => { eprintln!("[ny-compiler] child wait error: {}", e); break; }
Err(e) => { eprintln!("[ny-compiler] inline wait error: {}", e); break; }
}
}
let mut out_buf = Vec::new();
let mut err_buf = Vec::new();
if let Some(mut s) = ch_stdout { let _ = s.read_to_end(&mut out_buf); }
if let Some(mut s) = ch_stderr { let _ = s.read_to_end(&mut err_buf); }
if timed_out {
let head = String::from_utf8_lossy(&out_buf).chars().take(200).collect::<String>();
eprintln!("[ny-compiler] child timeout after {} ms; stdout(head)='{}'", timeout_ms, head.replace('\n', "\\n"));
eprintln!("[ny-compiler] inline timeout after {} ms; stdout(head)='{}'", timeout_ms, head.replace('\n', "\\n"));
}
raw = String::from_utf8_lossy(&out_buf).to_string();
}
@ -269,11 +323,14 @@ impl NyashRunner {
super::json_v0_bridge::maybe_dump_mir(&module);
let emit_only = std::env::var("NYASH_NY_COMPILER_EMIT_ONLY").unwrap_or_else(|_| "1".to_string()) == "1";
if emit_only { return false; }
// Prefer PyVM when requested AND the module contains BoxCalls (Stage-2 semantics)
let needs_pyvm = module.functions.values().any(|f| {
// Phase-15 policy: when NYASH_VM_USE_PY=1, prefer PyVM as reference executor
// regardless of BoxCall presence to ensure semantics parity (e.g., PHI merges).
let prefer_pyvm = std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1");
// Backward compatibility: if not preferring PyVM explicitly, still auto-enable when BoxCalls exist.
let needs_pyvm = !prefer_pyvm && module.functions.values().any(|f| {
f.blocks.values().any(|bb| bb.instructions.iter().any(|inst| matches!(inst, crate::mir::MirInstruction::BoxCall { .. })))
});
if needs_pyvm && std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1") {
if prefer_pyvm || needs_pyvm {
if let Ok(py3) = which::which("python3") {
let runner = std::path::Path::new("tools/pyvm_runner.py");
if runner.exists() {
@ -285,7 +342,8 @@ impl NyashRunner {
process::exit(1);
}
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
eprintln!("[Bridge] using PyVM (selfhost-fallback) → {}", mir_json_path.display());
let mode = if prefer_pyvm { "selfhost" } else { "selfhost-fallback" };
eprintln!("[Bridge] using PyVM ({}) → {}", mode, mir_json_path.display());
}
let entry = if module.functions.contains_key("Main.main") { "Main.main" } else if module.functions.contains_key("main") { "main" } else { "Main.main" };
let status = std::process::Command::new(py3)