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:
Selfhosting Dev
2025-09-07 20:23:39 +09:00
parent 1bb2d2db5b
commit caf0e922ef
13 changed files with 433 additions and 4 deletions

20
apps/selfhost/README.md Normal file
View File

@ -0,0 +1,20 @@
# SelfHosting Apps (Nyonly)
Purpose
- Keep selfhosting 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 crossproject coupling.
- If moving an existing `apps/*` item here, update docs/scripts accordingly.
- For namespace usage, pass `--using-path apps/selfhost:apps`.

View File

@ -0,0 +1,14 @@
# Ny Parser (v0) — Minimal Nyash-made Parser
- Scope: integers, + - * /, parentheses, and a single `return` statement.
- Output: JSON IR v0 as documented in CURRENT_TASK.md (Program/Return/Int/Binary).
Usage (Unix)
- echo "return 1+2*3" | ./tools/ny_parser_run.sh
Usage (Windows PowerShell)
- Get-Content .\apps\ny-mir-samples\arithmetic.nyash | .\tools\ny_parser_run.ps1
Notes
- This is a minimal educational parser to bootstrap the self-host loop.
- Errors print a JSON envelope: {"version":0,"kind":"Error",...}.

View File

@ -0,0 +1,28 @@
// Entry: read stdin, parse with ParserV0, print JSON IR or error JSON
include "./apps/selfhost/ny-parser-nyash/parser_minimal.nyash"
static box Main {
main(args) {
local console = new ConsoleBox()
// Read all stdin
local buf = ""
loop(true) {
local line = console.readLine()
if line == null { break }
buf = buf + line + "\n"
}
if buf == "" { buf = "return 0\n" }
local ir = ParserV0.parse_program(buf)
// If already an Error envelope, print as-is
local s = ir.as_any().toString()
if s.indexOf("\"kind\":\"Error\"") >= 0 {
console.log(s)
return 1
}
// Expect MapBox with Program; toJson available on MapBox
local json = ir.toJson()
console.log(json)
return 0
}
}

View File

@ -0,0 +1,87 @@
// Minimal recursive-descent parser for Ny v0 producing JSON IR v0 (MapBox)
include "./apps/selfhost/ny-parser-nyash/tokenizer.nyash"
static box ParserV0 {
init { tokens, pos }
parse_program(input) {
me.tokens = Tokenizer.tokenize(input)
// Error passthrough
if me.tokens.as_any().toString().indexOf("\"kind\":\"Error\"") >= 0 {
return me.tokens
}
me.pos = 0
local stmt = me.parse_stmt()
if stmt.as_any().toString().indexOf("\"kind\":\"Error\"") >= 0 { return stmt }
local body = new ArrayBox(); body.push(stmt)
local prog = new MapBox(); prog.set("version", 0); prog.set("kind", "Program"); prog.set("body", body)
return prog
}
parse_stmt() {
local tok = me.peek()
if tok.get("type") == "RETURN" {
me.next()
local expr = me.parse_expr()
if expr.as_any().toString().indexOf("\"kind\":\"Error\"") >= 0 { return expr }
local ret = new MapBox(); ret.set("type", "Return"); ret.set("expr", expr)
return ret
}
return me.err("Expected 'return'")
}
parse_expr() {
local left = me.parse_term()
loop(true) {
local t = me.peek(); local ty = t.get("type")
if ty == "+" || ty == "-" {
me.next(); local right = me.parse_term()
left = me.bin(ty, left, right)
} else { break }
}
return left
}
parse_term() {
local left = me.parse_factor()
loop(true) {
local t = me.peek(); local ty = t.get("type")
if ty == "*" || ty == "/" {
me.next(); local right = me.parse_factor()
left = me.bin(ty, left, right)
} else { break }
}
return left
}
parse_factor() {
local t = me.peek(); local ty = t.get("type")
if ty == "INT" {
me.next();
local node = new MapBox(); node.set("type", "Int"); node.set("value", t.get("value"))
return node
}
if ty == "(" {
me.next();
local e = me.parse_expr()
local r = me.peek(); if r.get("type") != ")" { return me.err(") expected") } else { me.next() }
return e
}
return me.err("factor expected")
}
// helpers
peek() { return me.tokens.get(me.pos) }
next() { me.pos = me.pos + 1; return me.tokens.get(me.pos-1) }
bin(op, lhs, rhs) {
local m = new MapBox(); m.set("type", "Binary"); m.set("op", op); m.set("lhs", lhs); m.set("rhs", rhs); return m
}
err(msg) {
local err = new MapBox(); err.set("version", 0); err.set("kind", "Error")
local e = new MapBox(); e.set("message", msg)
local sp = new MapBox(); sp.set("start", me.pos); sp.set("end", me.pos)
e.set("span", sp); err.set("error", e)
return err
}
}

View File

@ -0,0 +1,55 @@
// Minimal tokenizer for Ny v0 (ints, + - * /, ( ), return)
static box Tokenizer {
tokenize(input) {
local tokens = new ArrayBox()
local i = 0
local n = input.length()
// helper: skip whitespace
fn skip_ws() {
loop(i < n) {
local ch = input.substring(i, i+1)
if ch == " " || ch == "\t" || ch == "\r" || ch == "\n" { i = i + 1 } else { return }
}
}
// main loop
loop(i < n) {
skip_ws()
if i >= n { break }
local ch = input.substring(i, i+1)
if ch == "+" || ch == "-" || ch == "*" || ch == "/" || ch == "(" || ch == ")" {
local tok = new MapBox(); tok.set("type", ch)
tokens.push(tok); i = i + 1; continue
}
// keyword: return
if i + 6 <= n {
local kw = input.substring(i, i+6)
if kw == "return" {
local t = new MapBox(); t.set("type", "RETURN")
tokens.push(t); i = i + 6; continue
}
}
// integer literal
if ch >= "0" && ch <= "9" {
local j = i
loop(j < n) {
local cj = input.substring(j, j+1)
if cj >= "0" && cj <= "9" { j = j + 1 } else { break }
}
local num_str = input.substring(i, j)
local tnum = new MapBox(); tnum.set("type", "INT"); tnum.set("value", num_str)
tokens.push(tnum); i = j; continue
}
// unknown
local err = new MapBox(); err.set("version", 0); err.set("kind", "Error")
local e = new MapBox(); e.set("message", "Unknown token");
local sp = new MapBox(); sp.set("start", i); sp.set("end", i+1)
e.set("span", sp); err.set("error", e)
return err
}
// EOF
local eof = new MapBox(); eof.set("type", "EOF"); tokens.push(eof)
return tokens
}
}

View 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
}
}

View 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
}
}