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
This commit is contained in:
@ -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`)。
|
||||
|
||||
|
||||
8
Makefile
8
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
|
||||
|
||||
|
||||
20
apps/selfhost/README.md
Normal file
20
apps/selfhost/README.md
Normal file
@ -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`.
|
||||
@ -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) {
|
||||
@ -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 }
|
||||
253
apps/selfhost/tools/dep_tree.nyash
Normal file
253
apps/selfhost/tools/dep_tree.nyash
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
18
apps/selfhost/tools/dep_tree_main.nyash
Normal file
18
apps/selfhost/tools/dep_tree_main.nyash
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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]`.
|
||||
|
||||
8
tools/dep_tree.sh
Normal file
8
tools/dep_tree.sh
Normal file
@ -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"
|
||||
109
tools/dev_selfhost_loop.sh
Normal file
109
tools/dev_selfhost_loop.sh
Normal file
@ -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 <<EOF
|
||||
Usage: tools/dev_selfhost_loop.sh [options] [script.nyash] [-- ARGS]
|
||||
|
||||
Options:
|
||||
--watch Re-run on file changes (apps/**/*.nyash)
|
||||
--backend <mode> 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
|
||||
@ -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 } }'
|
||||
|
||||
Reference in New Issue
Block a user