From caf0e922eff88f013d56f44446f6ffc849f94960 Mon Sep 17 00:00:00 2001 From: Selfhosting Dev Date: Sun, 7 Sep 2025 20:23:39 +0900 Subject: [PATCH] selfhosting: move ny-parser-nyash into apps/selfhost/, add Ny-only dev loop and initial dep tree tools; add docs plan; make dev/dev-watch --- CURRENT_TASK.md | 2 +- Makefile | 8 + apps/selfhost/README.md | 20 ++ apps/{ => selfhost}/ny-parser-nyash/README.md | 0 .../{ => selfhost}/ny-parser-nyash/main.nyash | 2 +- .../ny-parser-nyash/parser_minimal.nyash | 2 +- .../ny-parser-nyash/tokenizer.nyash | 0 apps/selfhost/tools/dep_tree.nyash | 253 ++++++++++++++++++ apps/selfhost/tools/dep_tree_main.nyash | 18 ++ dev/selfhosting/README.md | 13 + tools/dep_tree.sh | 8 + tools/dev_selfhost_loop.sh | 109 ++++++++ tools/ny_parser_run.sh | 2 +- 13 files changed, 433 insertions(+), 4 deletions(-) create mode 100644 apps/selfhost/README.md rename apps/{ => selfhost}/ny-parser-nyash/README.md (100%) rename apps/{ => selfhost}/ny-parser-nyash/main.nyash (91%) rename apps/{ => selfhost}/ny-parser-nyash/parser_minimal.nyash (98%) rename apps/{ => selfhost}/ny-parser-nyash/tokenizer.nyash (100%) create mode 100644 apps/selfhost/tools/dep_tree.nyash create mode 100644 apps/selfhost/tools/dep_tree_main.nyash create mode 100644 tools/dep_tree.sh create mode 100644 tools/dev_selfhost_loop.sh diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index c449efd3..d4b11bf3 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -630,7 +630,7 @@ Phase A 進捗(実施済) - 既存 Workspace は維持(`crates/*`)。 - 方針: crates 側は変更せず「Nyash スクリプト + nyash.exe」だけで実装・運用(Windows優先)。 - 例: `C:\git\nyash-project\nyash_self\nyash` 直下で `target\release\nyash` 実行。 -- Nyash 製パーサは `apps/ny-parser-nyash/`(Nyashコード)として配置(最初は最小サブセット)。 +- Nyash 製パーサは `apps/selfhost/ny-parser-nyash/`(Nyashコード)として配置(最初は最小サブセット)。 - MIR 解釈層は既存 `backend/mir_interpreter.rs` と `runner/modes/mir_interpreter.rs` を拡充。 - AOT 関連の雛形は `src/backend/cranelift/` に維持(feature gate: `cranelift-aot`)。 diff --git a/Makefile b/Makefile index 2a28e97b..494bc3c8 100644 --- a/Makefile +++ b/Makefile @@ -33,3 +33,11 @@ fmt: lint: cargo clippy --all-targets --all-features -- -D warnings || true + +# --- Self-hosting dev helpers (Ny-only inner loop) --- +dev: + ./tools/dev_selfhost_loop.sh --std -v -- --using-path apps/selfhost:apps apps/selfhost-minimal/main.nyash + +dev-watch: + ./tools/dev_selfhost_loop.sh --watch --std -v -- --using-path apps/selfhost:apps apps/selfhost-minimal/main.nyash + diff --git a/apps/selfhost/README.md b/apps/selfhost/README.md new file mode 100644 index 00000000..b3a8b70e --- /dev/null +++ b/apps/selfhost/README.md @@ -0,0 +1,20 @@ +# Self‑Hosting Apps (Ny‑only) + +Purpose +- Keep self‑hosting Ny scripts isolated from general `apps/` noise. +- Enable fast inner loop without touching Rust unless necessary. + +Conventions +- Entry scripts live under this folder; prefer minimal dependencies. +- Use `--using-path apps/selfhost:apps` when resolving modules. +- Prefer VM (`--backend vm`) for speed and stability. + +Quickstart +- Run minimal sample: `make dev` (uses `apps/selfhost-minimal/main.nyash`) +- Watch changes: `make dev-watch` +- Run parser Ny project: `./tools/dev_selfhost_loop.sh --std -v --backend vm -- apps/selfhost/ny-parser-nyash/main.nyash` + +Guidelines +- Keep files small and composable; avoid cross‑project coupling. +- If moving an existing `apps/*` item here, update docs/scripts accordingly. +- For namespace usage, pass `--using-path apps/selfhost:apps`. diff --git a/apps/ny-parser-nyash/README.md b/apps/selfhost/ny-parser-nyash/README.md similarity index 100% rename from apps/ny-parser-nyash/README.md rename to apps/selfhost/ny-parser-nyash/README.md diff --git a/apps/ny-parser-nyash/main.nyash b/apps/selfhost/ny-parser-nyash/main.nyash similarity index 91% rename from apps/ny-parser-nyash/main.nyash rename to apps/selfhost/ny-parser-nyash/main.nyash index 08badc4e..6cf31f02 100644 --- a/apps/ny-parser-nyash/main.nyash +++ b/apps/selfhost/ny-parser-nyash/main.nyash @@ -1,6 +1,6 @@ // Entry: read stdin, parse with ParserV0, print JSON IR or error JSON -include "./apps/ny-parser-nyash/parser_minimal.nyash" +include "./apps/selfhost/ny-parser-nyash/parser_minimal.nyash" static box Main { main(args) { diff --git a/apps/ny-parser-nyash/parser_minimal.nyash b/apps/selfhost/ny-parser-nyash/parser_minimal.nyash similarity index 98% rename from apps/ny-parser-nyash/parser_minimal.nyash rename to apps/selfhost/ny-parser-nyash/parser_minimal.nyash index 6a2b8c18..e905c6a8 100644 --- a/apps/ny-parser-nyash/parser_minimal.nyash +++ b/apps/selfhost/ny-parser-nyash/parser_minimal.nyash @@ -1,6 +1,6 @@ // Minimal recursive-descent parser for Ny v0 producing JSON IR v0 (MapBox) -include "./apps/ny-parser-nyash/tokenizer.nyash" +include "./apps/selfhost/ny-parser-nyash/tokenizer.nyash" static box ParserV0 { init { tokens, pos } diff --git a/apps/ny-parser-nyash/tokenizer.nyash b/apps/selfhost/ny-parser-nyash/tokenizer.nyash similarity index 100% rename from apps/ny-parser-nyash/tokenizer.nyash rename to apps/selfhost/ny-parser-nyash/tokenizer.nyash diff --git a/apps/selfhost/tools/dep_tree.nyash b/apps/selfhost/tools/dep_tree.nyash new file mode 100644 index 00000000..7e1a9588 --- /dev/null +++ b/apps/selfhost/tools/dep_tree.nyash @@ -0,0 +1,253 @@ +// dep_tree.nyash — Build dependency info for a Nyash script (include + using/module) + +static box DepTree { + init { visited } + + // Public API: build tree for entry path + build(entry_path) { + me.visited = new MapBox() + return me.node(entry_path) + } + + // Read entire file as string via FileBox + read_file(path) { + local fb = new FileBox() + local ok = fb.open(path, "r") + if ok == false { return null } + local content = fb.read() + fb.close() + return content + } + + // Extract include paths from source: include "./path.nyash" + extract_includes(src) { + local out = new ArrayBox() + if src == null { return out } + local i = 0 + local n = src.length() + loop(i < n) { + local j = src.indexOf("include \"", i) + if j < 0 { break } + local k = j + 9 // after include " + local q = src.indexOf("\"", k) + if q < 0 { break } + out.push(src.substring(k, q)) + i = q + 1 + } + return out + } + + // Extract using directives (script lines): + // using ns + // using ns as Alias + // using "path" as Name + // and comment form: // @using ns[ as Alias] + extract_usings(src) { + local out = new ArrayBox() + if src == null { return out } + local lines = me.split_lines(src) + local i = 0 + loop(i < lines.length()) { + local line = lines.get(i).trim() + local t = line + if t.startsWith("// @using ") { t = t.substring(10, t.length()).trim() } + else if t.startsWith("using ") { t = t.substring(6, t.length()).trim() } + else { i = i + 1; continue } + + // optional trailing semicolon + if t.endsWith(";") { t = t.substring(0, t.length()-1).trim() } + + local rec = new MapBox() + // Split alias + local as_pos = t.indexOf(" as ") + local target = t + local alias = null + if as_pos >= 0 { + target = t.substring(0, as_pos).trim() + alias = t.substring(as_pos+4, t.length()).trim() + } + rec.set("target", target) + if alias != null { rec.set("alias", alias) } + // classify + if target.startsWith("\"") || target.startsWith("./") || target.startsWith("/") || target.endsWith(".nyash") { + rec.set("kind", "path") + // strip quotes + if target.startsWith("\"") { rec.set("target", target.substring(1, target.length()-1)) } + } else { + rec.set("kind", "ns") + } + out.push(rec) + i = i + 1 + } + return out + } + + // Extract modules mapping from // @module ns=path + extract_modules(src) { + local out = new ArrayBox() + if src != null { + local lines = me.split_lines(src) + local i = 0 + loop(i < lines.length()) { + local line = lines.get(i).trim() + if line.startsWith("// @module ") { + local rest = line.substring(11, line.length()).trim() + local eq = rest.indexOf("=") + if eq > 0 { + local ns = rest.substring(0, eq).trim() + local path = rest.substring(eq+1, rest.length()).trim() + path = me.strip_quotes(path) + local m = new MapBox(); m.set("ns", ns); m.set("path", path); out.push(m) + } + } + i = i + 1 + } + } + return out + } + + // Build a node: { path, includes:[...], using:[...], modules:[...], children:[...] } + node(path) { + if me.visited.has(path) { return me.leaf(path) } + me.visited.set(path, true) + + local m = new MapBox(); m.set("path", path) + local src = me.read_file(path) + if src == null { m.set("error", "read_fail"); me.ensure_arrays(m); return m } + + local base_dir = me.dirname(path) + + // includes + local incs = me.extract_includes(src) + m.set("includes", incs) + // usings + local us = me.extract_usings(src) + m.set("using", us) + // modules mapping (script-level) + local mods = me.extract_modules(src) + m.set("modules", mods) + + // children = includes + resolved using(path) + resolved using(ns via search paths) + local children = new ArrayBox() + // include children + local i = 0 + loop(i < incs.length()) { + local p = incs.get(i) + local child_path = me.resolve_path(base_dir, p) + children.push(me.node(child_path)) + i = i + 1 + } + // using(path) children + i = 0 + loop(i < us.length()) { + local u = us.get(i) + if u.get("kind") == "path" { + local p = u.get("target") + local child_path = me.resolve_path(base_dir, p) + children.push(me.node(child_path)) + } + i = i + 1 + } + // using(ns) children resolved via search paths + local search = me.default_using_paths() + i = 0 + loop(i < us.length()) { + local u = us.get(i) + if u.get("kind") == "ns" { + local ns = u.get("target") + local rel = ns.replace(".", "/") + ".nyash" + local found = me.search_in_paths(base_dir, search, rel) + if found != null { children.push(me.node(found)) } + else { + // annotate unresolved + u.set("unresolved", true) + u.set("hint", rel) + } + } + i = i + 1 + } + + m.set("children", children) + return m + } + + // Helpers + ensure_arrays(m) { m.set("includes", new ArrayBox()); m.set("using", new ArrayBox()); m.set("modules", new ArrayBox()); m.set("children", new ArrayBox()) } + + default_using_paths() { + // Best-effort defaults prioritized for selfhost + local arr = new ArrayBox() + arr.push("apps/selfhost"); arr.push("apps"); arr.push("lib"); arr.push(".") + return arr + } + + split_lines(src) { + local out = new ArrayBox() + local i = 0; local n = src.length(); local start = 0 + loop(i <= n) { + if i == n || src.substring(i, i+1) == "\n" { + out.push(src.substring(start, i)) + start = i + 1 + } + i = i + 1 + } + return out + } + + strip_quotes(s) { + if s == null { return null } + if s.length() >= 2 && s.substring(0,1) == "\"" && s.substring(s.length()-1, s.length()) == "\"" { + return s.substring(1, s.length()-1) + } + return s + } + + dirname(path) { + local pb = new PathBox(); + local d = pb.dirname(path) + if d != null { return d } + local i = path.lastIndexOf("/") + if i < 0 { return "." } + return path.substring(0, i) + } + + resolve_path(base, rel) { + if rel.indexOf("/") == 0 { return rel } + if rel.startsWith("./") || rel.startsWith("../") { + local pb = new PathBox(); + local j = pb.join(base, rel) + if j != null { return j } + return base + "/" + rel + } + return rel + } + + search_in_paths(base, paths, rel) { + // try relative to file first + local pb = new PathBox(); + local j = pb.join(base, rel) + if me.file_exists(j) { return j } + // then search list + local i = 0 + loop(i < paths.length()) { + local p = paths.get(i) + local cand = pb.join(p, rel) + if me.file_exists(cand) { return cand } + i = i + 1 + } + return null + } + + file_exists(path) { + // Use FileBox.open in read mode as exists check + local fb = new FileBox(); + local ok = fb.open(path, "r") + if ok == false { return false } + fb.close(); return true + } + + leaf(path) { + local m = new MapBox(); m.set("path", path); m.set("children", new ArrayBox()); m.set("note", "visited") + return m + } +} diff --git a/apps/selfhost/tools/dep_tree_main.nyash b/apps/selfhost/tools/dep_tree_main.nyash new file mode 100644 index 00000000..a3da4c67 --- /dev/null +++ b/apps/selfhost/tools/dep_tree_main.nyash @@ -0,0 +1,18 @@ +// dep_tree_main.nyash — entry script to print JSON tree + +include "./apps/selfhost/tools/dep_tree.nyash" + +static box Main { + main(args) { + local console = new ConsoleBox() + local path = null + if args != null && args.length() >= 1 { path = args.get(0) } + if path == null || path == "" { + // default sample + path = "apps/selfhost/ny-parser-nyash/main.nyash" + } + local tree = DepTree.build(path) + console.println(tree.toJson()) + return 0 + } +} diff --git a/dev/selfhosting/README.md b/dev/selfhosting/README.md index 40419b38..1ab3c75d 100644 --- a/dev/selfhosting/README.md +++ b/dev/selfhosting/README.md @@ -32,3 +32,16 @@ Tips - For debug, set `NYASH_CLI_VERBOSE=1`. - Keep temp artifacts under this folder (`dev/selfhosting/_tmp/`) to avoid polluting repo root. + + +Dev Loop (Ny-only) + +- One-off run (VM): `./tools/dev_selfhost_loop.sh apps/selfhost-minimal/main.nyash` +- Watch + std libs: `./tools/dev_selfhost_loop.sh --watch --std apps/selfhost/ny-parser-nyash/main.nyash` +- Make targets: + - `make dev` (VM, std on, verbose) + - `make dev-watch` (watch mode) + +Notes +- Rebuild Rust only when core changes; Ny scripts reload on each run. +- Flags: `--backend mir|vm`, `-v` for verbose, `--std` to load `[ny_plugins]`. diff --git a/tools/dep_tree.sh b/tools/dep_tree.sh new file mode 100644 index 00000000..31f8b5f9 --- /dev/null +++ b/tools/dep_tree.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ENTRY=${1:-apps/selfhost/ny-parser-nyash/main.nyash} + +NYASH_DISABLE_PLUGINS=0 NYASH_CLI_VERBOSE=0 \ + "$ROOT_DIR/target/release/nyash" --backend vm \ + "$ROOT_DIR/apps/selfhost/tools/dep_tree_main.nyash" <<<"$ENTRY" diff --git a/tools/dev_selfhost_loop.sh b/tools/dev_selfhost_loop.sh new file mode 100644 index 00000000..aecff672 --- /dev/null +++ b/tools/dev_selfhost_loop.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Nyash self-hosting dev loop helper +# Goals: +# - Avoid repeated Rust builds; iterate on .nyash scripts only +# - One-time ensure binary exists; then run with VM (default) or MIR +# - Optional watch mode that re-runs on file changes (uses entr/inotifywait if available) + +ROOT_DIR=$(cd "$(dirname "$0")/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +SCRIPT="apps/selfhost-minimal/main.nyash" +BACKEND="vm" +WATCH=0 +LOAD_STD=0 +VERBOSE=0 +EXTRA_ARGS=() + +usage() { + cat < interpreter|mir|vm (default: vm) + --std Load Ny std scripts from nyash.toml ([ny_plugins]) + -v, --verbose Verbose CLI output + -h, --help Show this help + +Examples: + # One-off run (VM), minimal selfhost sample + tools/dev_selfhost_loop.sh apps/selfhost-minimal/main.nyash + + # Watch mode with Ny std libs loaded + tools/dev_selfhost_loop.sh --watch --std apps/selfhost/ny-parser-nyash/main.nyash + +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --watch) WATCH=1; shift ;; + --backend) BACKEND="$2"; shift 2 ;; + --std) LOAD_STD=1; shift ;; + -v|--verbose) VERBOSE=1; shift ;; + -h|--help) usage; exit 0 ;; + --) shift; EXTRA_ARGS=("${@}"); break ;; + *.nyash) SCRIPT="$1"; shift ;; + *) EXTRA_ARGS+=("$1"); shift ;; + esac +done + +if [[ ! -f "$BIN" ]]; then + echo "[dev] nyash binary not found; building release (one-time)..." + (cd "$ROOT_DIR" && cargo build --release --features cranelift-jit) +fi + +run_once() { + local envs=("NYASH_DISABLE_PLUGINS=1") + if [[ "$LOAD_STD" -eq 1 ]]; then + envs+=("NYASH_LOAD_NY_PLUGINS=1") + fi + if [[ "$VERBOSE" -eq 1 ]]; then + envs+=("NYASH_CLI_VERBOSE=1") + fi + echo "[dev] running: ${envs[*]} $BIN --backend $BACKEND $SCRIPT ${EXTRA_ARGS[*]}" + (cd "$ROOT_DIR" && \ + env ${envs[@]} "$BIN" --backend "$BACKEND" "$SCRIPT" "${EXTRA_ARGS[@]}" ) +} + +if [[ "$WATCH" -eq 0 ]]; then + run_once + exit $? +fi + +# Watch mode +echo "[dev] watch mode ON (backend=$BACKEND, std=$LOAD_STD)" +FILES_CMD="rg --files --glob 'apps/**/*.nyash'" + +if command -v entr >/dev/null 2>&1; then + # Use entr for reliable cross-platform watching + echo "[dev] using 'entr' for file watching" + # Feed a stable, sorted file list to entr; rerun on any change + eval "$FILES_CMD" | sort | entr -rs "bash -lc '$(printf %q "$ROOT_DIR")/tools/dev_selfhost_loop.sh --backend $(printf %q "$BACKEND") $( ((LOAD_STD)) && echo --std ) $( ((VERBOSE)) && echo -v ) $(printf %q "$SCRIPT") ${EXTRA_ARGS:+-- ${EXTRA_ARGS[*]@Q}}'" + exit $? +fi + +if command -v inotifywait >/dev/null 2>&1; then + echo "[dev] using 'inotifywait' for file watching" + while true; do + run_once || true + # Block until any .nyash under apps changes + inotifywait -qq -r -e close_write,create,delete,move "$ROOT_DIR/apps" || true + done + exit 0 +fi + +# Fallback: naive polling on mtime hash every 1s +echo "[dev] no watcher found; falling back to 1s polling" +prev_hash="" +while true; do + cur_hash=$( (cd "$ROOT_DIR" && eval "$FILES_CMD" | xargs -r stat -c '%Y %n' 2>/dev/null | md5sum | awk '{print $1}') ) + if [[ "$cur_hash" != "$prev_hash" ]]; then + prev_hash="$cur_hash" + run_once || true + fi + sleep 1 +done diff --git a/tools/ny_parser_run.sh b/tools/ny_parser_run.sh index 16e8c3ab..be3c70ea 100644 --- a/tools/ny_parser_run.sh +++ b/tools/ny_parser_run.sh @@ -4,5 +4,5 @@ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) NYASH_JSON_ONLY=1 NYASH_DISABLE_PLUGINS=1 NYASH_CLI_VERBOSE=0 \ - ${ROOT_DIR}/target/release/nyash ${ROOT_DIR}/apps/ny-parser-nyash/main.nyash \ + ${ROOT_DIR}/target/release/nyash ${ROOT_DIR}/apps/selfhost/ny-parser-nyash/main.nyash \ | awk 'BEGIN{printed=0} { if (!printed && $0 ~ /^\s*\{/){ print; printed=1 } }'