#!/usr/bin/env python3 import sys, os, argparse def read_text(path: str) -> str: with open(path, 'r', encoding='utf-8') as f: return f.read() def load_toml_modules(toml_path: str): modules = {} using_paths = [] try: txt = read_text(toml_path) except Exception: return modules, using_paths section = None for line in txt.splitlines(): s = line.strip() if not s: continue if s.startswith('[') and s.endswith(']'): section = s[1:-1].strip() continue if section == 'modules': # dotted key: a.b.c = "path" if '=' in s: k, v = s.split('=', 1) key = k.strip() val = v.strip().strip('"') if key and val: modules[key] = val if section == 'using': # paths = ["apps", "lib", "."] if s.startswith('paths') and '=' in s: _, arr = s.split('=', 1) arr = arr.strip() if arr.startswith('[') and arr.endswith(']'): body = arr[1:-1] parts = [p.strip().strip('"') for p in body.split(',') if p.strip()] using_paths = [p for p in parts if p] return modules, using_paths def brace_delta_ignoring_strings(text: str) -> int: i = 0 n = len(text) delta = 0 in_str = False in_sl = False in_ml = False while i < n: ch = text[i] # single-line comment if in_sl: if ch == '\n': in_sl = False i += 1 continue # multi-line comment if in_ml: if ch == '*' and i + 1 < n and text[i + 1] == '/': in_ml = False i += 2 continue i += 1 continue # string if in_str: if ch == '\\': i += 2 continue if ch == '"': in_str = False i += 1 continue i += 1 continue # enter states if ch == '"': in_str = True i += 1 continue if ch == '/' and i + 1 < n and text[i + 1] == '/': in_sl = True i += 2 continue if ch == '/' and i + 1 < n and text[i + 1] == '*': in_ml = True i += 2 continue # count braces if ch == '{': delta += 1 elif ch == '}': delta -= 1 i += 1 return delta def find_balanced(text: str, start: int, open_ch: str, close_ch: str) -> int: i = start n = len(text) if i >= n or text[i] != open_ch: return -1 depth = 0 while i < n: ch = text[i] if ch == '"': i += 1 while i < n: c = text[i] if c == '\\': i += 2 continue if c == '"': i += 1 break i += 1 continue if ch == open_ch: depth += 1 if ch == close_ch: depth -= 1 if depth == 0: return i i += 1 return -1 def dedup_boxes(text: str) -> str: seen = set() out = [] i = 0 n = len(text) tok = 'static box ' while i < n: if text.startswith(tok, i): j = i + len(tok) # read identifier name = [] while j < n and (text[j] == '_' or text[j].isalnum()): name.append(text[j]); j += 1 # skip ws to '{' while j < n and text[j].isspace(): j += 1 if j < n and text[j] == '{': end = find_balanced(text, j, '{', '}') if end < 0: end = j block = text[i:end+1] nm = ''.join(name) if nm in seen: i = end + 1 continue seen.add(nm) out.append(block) i = end + 1 continue # default: copy one char out.append(text[i]) i += 1 return ''.join(out) def dedup_fn_prints_in_slice(text: str) -> str: # limited: within static box MiniVmPrints, keep only first definition of print_prints_in_slice out = [] i = 0 n = len(text) tok = 'static box ' while i < n: if text.startswith(tok, i): j = i + len(tok) name = [] while j < n and (text[j] == '_' or text[j].isalnum()): name.append(text[j]); j += 1 while j < n and text[j].isspace(): j += 1 if j < n and text[j] == '{': end = find_balanced(text, j, '{', '}') if end < 0: end = j header = text[i:j+1] body = text[j+1:end] nm = ''.join(name) if nm == 'MiniVmPrints': kept = False body_out = [] p = 0 m = len(body) while p < m: # find line start ls = p if ls > 0: while ls < m and body[ls-1] != '\n': ls += 1 if ls >= m: break # skip ws q = ls while q < m and body[q].isspace() and body[q] != '\n': q += 1 rem = body[q:q+64] if rem.startswith('print_prints_in_slice('): # find def body r = q depth = 0 in_s = False while r < m: c = body[r] if in_s: if c == '\\': r += 2; continue if c == '"': in_s = False r += 1; continue if c == '"': in_s = True; r += 1; continue if c == '(': depth += 1; r += 1; continue if c == ')': depth -= 1; r += 1 if depth <= 0: break continue r += 1 while r < m and body[r].isspace(): r += 1 if r < m and body[r] == '{': t = r d2 = 0 in_s2 = False while t < m: c2 = body[t] if in_s2: if c2 == '\\': t += 2; continue if c2 == '"': in_s2 = False t += 1; continue if c2 == '"': in_s2 = True; t += 1; continue if c2 == '{': d2 += 1 if c2 == '}': d2 -= 1 if d2 == 0: t += 1; break t += 1 # start-of-line for pretty include sol = q while sol > 0 and body[sol-1] != '\n': sol -= 1 if not kept: body_out.append(body[sol:t]) kept = True p = t continue # default copy this line eol = ls while eol < m and body[eol] != '\n': eol += 1 body_out.append(body[ls:eol+1]) p = eol + 1 new_body = ''.join(body_out) out.append(header) out.append(new_body) out.append('}') i = end + 1 continue # non-target box out.append(text[i:end+1]) i = end + 1 continue out.append(text[i]); i += 1 return ''.join(out) def combine(entry: str, fix_braces: bool, dedup_box: bool, dedup_fn: bool, seam_debug: bool) -> str: repo_root = os.getcwd() modules, using_paths = load_toml_modules(os.path.join(repo_root, 'nyash.toml')) visited = set() def resolve_ns(ns: str) -> str | None: if ns in modules: return modules[ns] # try using paths as base dirs for base in using_paths or []: cand = os.path.join(base, *(ns.split('.'))) + '.nyash' if os.path.exists(cand): return cand return None def strip_and_inline(path: str) -> str: abspath = path if not os.path.isabs(abspath): abspath = os.path.abspath(path) key = os.path.normpath(abspath) if key in visited: return '' visited.add(key) code = read_text(abspath) out_lines = [] used = [] # list[(target, alias_or_path)] ; alias_or_path: None|alias|path for line in code.splitlines(): t = line.lstrip() if t.startswith('using '): rest = t[len('using '):].strip() if rest.endswith(';'): rest = rest[:-1].strip() if ' as ' in rest: tgt, alias = rest.split(' as ', 1) tgt = tgt.strip(); alias = alias.strip() else: tgt, alias = rest, None # path vs ns is_path = tgt.startswith('"') or tgt.startswith('./') or tgt.startswith('/') or tgt.endswith('.nyash') if is_path: path_tgt = tgt.strip('"') name = alias or os.path.splitext(os.path.basename(path_tgt))[0] used.append((name, path_tgt)) else: used.append((tgt, alias)) continue out_lines.append(line) out = '\n'.join(out_lines) + '\n' prelude = [] for ns, alias in used: path_tgt = None if alias is not None and (ns.startswith('/') or ns.startswith('./') or ns.endswith('.nyash')): path_tgt = ns else: if alias is None: # direct namespace path_tgt = resolve_ns(ns) else: # alias ns path_tgt = resolve_ns(ns) if not path_tgt: continue # resolve relative to current file if not os.path.isabs(path_tgt): cand = os.path.join(os.path.dirname(abspath), path_tgt) if os.path.exists(cand): path_tgt = cand if not os.path.exists(path_tgt): continue inlined = strip_and_inline(path_tgt) if inlined: prelude.append(inlined) prelude_text = ''.join(prelude) # seam debug if seam_debug: tail = prelude_text[-160:].replace('\n', '\\n') head = out[:160].replace('\n', '\\n') sys.stderr.write(f"[using][seam] prelude_tail=<<<{tail}>>>\n") sys.stderr.write(f"[using][seam] body_head =<<<{head}>>>\n") # seam join prelude_clean = prelude_text.rstrip('\n\r') combined = prelude_clean + '\n' + out if fix_braces: delta = brace_delta_ignoring_strings(prelude_clean) if delta > 0: sys.stderr.write(f"[using][seam] fix: appending {delta} '}}' before body\n") combined = prelude_clean + '\n' + ('}\n' * delta) + out # dedups if dedup_box: combined = dedup_boxes(combined) if dedup_fn: combined = dedup_fn_prints_in_slice(combined) return combined return strip_and_inline(entry) def main(): ap = argparse.ArgumentParser() ap.add_argument('--entry', required=True) ap.add_argument('--fix-braces', action='store_true') ap.add_argument('--dedup-box', action='store_true') ap.add_argument('--dedup-fn', action='store_true') ap.add_argument('--seam-debug', action='store_true') args = ap.parse_args() text = combine(args.entry, args.fix_braces, args.dedup_box, args.dedup_fn, args.seam_debug) sys.stdout.write(text) if __name__ == '__main__': main()