diff --git a/main.py b/main.py index 459dc62..b286eb1 100644 --- a/main.py +++ b/main.py @@ -4649,6 +4649,7 @@ def _ct_lexer_push_back(vm: CompileTimeVM) -> None: vm.push(lexer) + # --------------------------------------------------------------------------- # Runtime intrinsics that cannot run as native JIT (for --ct-run-main) # --------------------------------------------------------------------------- @@ -5694,9 +5695,10 @@ class DocEntry: _DOC_STACK_RE = re.compile(r"^\s*#\s*([^\s]+)\s*(.*)$") -_DOC_WORD_RE = re.compile(r"^\s*word\s+([^\s]+)\b") +_DOC_WORD_RE = re.compile(r"^\s*(?:inline\s+)?word\s+([^\s]+)\b") _DOC_ASM_RE = re.compile(r"^\s*:asm\s+([^\s{]+)") _DOC_PY_RE = re.compile(r"^\s*:py\s+([^\s{]+)") +_DOC_MACRO_RE = re.compile(r"^\s*macro\s+([^\s]+)") def _extract_stack_comment(text: str) -> Optional[Tuple[str, str]]: @@ -5712,11 +5714,15 @@ def _extract_stack_comment(text: str) -> Optional[Tuple[str, str]]: return name, tail -def _extract_definition_name(text: str) -> Optional[Tuple[str, str]]: +def _extract_definition_name(text: str, *, include_macros: bool = False) -> Optional[Tuple[str, str]]: for kind, regex in (("word", _DOC_WORD_RE), ("asm", _DOC_ASM_RE), ("py", _DOC_PY_RE)): match = regex.match(text) if match is not None: return kind, match.group(1) + if include_macros: + match = _DOC_MACRO_RE.match(text) + if match is not None: + return "macro", match.group(1) return None @@ -5763,6 +5769,7 @@ def _scan_doc_file( *, include_undocumented: bool = False, include_private: bool = False, + include_macros: bool = False, ) -> List[DocEntry]: try: text = path.read_text(encoding="utf-8", errors="ignore") @@ -5774,7 +5781,7 @@ def _scan_doc_file( defined_names: Set[str] = set() for idx, line in enumerate(lines): - parsed = _extract_definition_name(line) + parsed = _extract_definition_name(line, include_macros=include_macros) if parsed is None: continue kind, name = parsed @@ -5839,6 +5846,7 @@ def collect_docs( *, include_undocumented: bool = False, include_private: bool = False, + include_macros: bool = False, include_tests: bool = False, ) -> List[DocEntry]: entries: List[DocEntry] = [] @@ -5848,6 +5856,7 @@ def collect_docs( doc_file, include_undocumented=include_undocumented, include_private=include_private, + include_macros=include_macros, ) ) # Deduplicate by symbol name; keep first (roots/files are stable-sorted) @@ -5944,7 +5953,12 @@ def _filter_docs(entries: Sequence[DocEntry], query: str) -> List[DocEntry]: return [entry for _, entry in ranked] -def _run_docs_tui(entries: Sequence[DocEntry], initial_query: str = "") -> int: +def _run_docs_tui( + entries: Sequence[DocEntry], + initial_query: str = "", + *, + reload_fn: Optional[Callable[..., List[DocEntry]]] = None, +) -> int: if not entries: print("[info] no documentation entries found") return 0 @@ -5961,6 +5975,141 @@ def _run_docs_tui(entries: Sequence[DocEntry], initial_query: str = "") -> int: import curses + _MODE_BROWSE = 0 + _MODE_SEARCH = 1 + _MODE_DETAIL = 2 + _MODE_FILTER = 3 + + _FILTER_KINDS = ["all", "word", "asm", "py", "macro"] + + def _parse_sig_counts(effect: str) -> Tuple[int, int]: + """Parse stack effect to (n_args, n_returns). + + Counts all named items (excluding ``*``) on each side of ``->``. + Items before ``|`` are deeper stack elements; items after are top. + Both count as args/returns. + + Handles dual-return with ``||``: + ``[* | x] -> [* | y] || [*, x | z]`` + Takes the first branch for counting. + Returns (-1, -1) for unparseable effects. + """ + if not effect or "->" not in effect: + return (-1, -1) + # Split off dual-return: take first branch + main = effect.split("||")[0].strip() + parts = main.split("->", 1) + if len(parts) != 2: + return (-1, -1) + lhs, rhs = parts[0].strip(), parts[1].strip() + + def _count_items(side: str) -> int: + s = side.strip() + if s.startswith("["): + s = s[1:] + if s.endswith("]"): + s = s[:-1] + s = s.strip() + if not s: + return 0 + # Flatten both sides of pipe and count all non-* items + all_items = s.replace("|", ",") + return len([x.strip() for x in all_items.split(",") + if x.strip() and x.strip() != "*"]) + + return (_count_items(lhs), _count_items(rhs)) + + def _safe_addnstr(scr: Any, y: int, x: int, text: str, maxlen: int, attr: int = 0) -> None: + h, w = scr.getmaxyx() + if y < 0 or y >= h or x >= w: + return + maxlen = min(maxlen, w - x) + if maxlen <= 0: + return + try: + scr.addnstr(y, x, text, maxlen, attr) + except curses.error: + pass + + def _build_detail_lines(entry: DocEntry, width: int) -> List[str]: + lines: List[str] = [] + lines.append(f"{'Name:':<14} {entry.name}") + lines.append(f"{'Kind:':<14} {entry.kind}") + if entry.stack_effect: + lines.append(f"{'Stack effect:':<14} {entry.stack_effect}") + else: + lines.append(f"{'Stack effect:':<14} (none)") + lines.append(f"{'File:':<14} {entry.path}:{entry.line}") + lines.append("") + if entry.description: + lines.append("Description:") + # Word-wrap description + words = entry.description.split() + current: List[str] = [] + col = 2 # indent + for w in words: + if current and col + 1 + len(w) > width - 2: + lines.append(" " + " ".join(current)) + current = [w] + col = 2 + len(w) + else: + current.append(w) + col += 1 + len(w) if current else len(w) + if current: + lines.append(" " + " ".join(current)) + else: + lines.append("(no description)") + lines.append("") + # Show source context + lines.append("Source context:") + try: + src_lines = entry.path.read_text(encoding="utf-8", errors="ignore").splitlines() + start = max(0, entry.line - 1) + if entry.kind == "word": + # Depth-tracking: word/if/while/for/begin/with open blocks closed by 'end' + _block_openers = {"word", "if", "while", "for", "begin", "with"} + depth = 0 + end = min(len(src_lines), start + 200) + for i in range(start, end): + stripped = src_lines[i].strip() + # Strip comments (# to end of line, but not inside strings) + code = stripped.split("#", 1)[0].strip() if "#" in stripped else stripped + # Count all block openers and 'end' tokens on the line + for tok in code.split(): + if tok in _block_openers: + depth += 1 + elif tok == "end": + depth -= 1 + prefix = f" {i + 1:4d}| " + lines.append(prefix + src_lines[i]) + if depth <= 0 and i > start: + break + elif entry.kind in ("asm", "py"): + # Show until closing brace + a few extra lines of context + end = min(len(src_lines), start + 200) + found_close = False + extra_after = 0 + for i in range(start, end): + prefix = f" {i + 1:4d}| " + lines.append(prefix + src_lines[i]) + stripped = src_lines[i].strip() + if not found_close and stripped in ("}", "};") and i > start: + found_close = True + extra_after = 0 + continue + if found_close: + extra_after += 1 + if extra_after >= 3 or not stripped: + break + else: + end = min(len(src_lines), start + 30) + for i in range(start, end): + prefix = f" {i + 1:4d}| " + lines.append(prefix + src_lines[i]) + except Exception: + lines.append(" (unable to read source)") + return lines + def _app(stdscr: Any) -> int: try: curses.curs_set(0) @@ -5968,16 +6117,377 @@ def _run_docs_tui(entries: Sequence[DocEntry], initial_query: str = "") -> int: pass stdscr.keypad(True) + nonlocal entries query = initial_query selected = 0 scroll = 0 + mode = _MODE_BROWSE + + # Search mode state + search_buf = query + + # Detail mode state + detail_scroll = 0 + detail_lines: List[str] = [] + + # Filter mode state + filter_kind_idx = 0 # index into _FILTER_KINDS + filter_field = 0 # 0=kind, 1=args, 2=returns, 3=show_private, 4=show_macros, 5=extra_path, 6=files + filter_file_scroll = 0 + filter_file_cursor = 0 + filter_args = -1 # -1 = any + filter_returns = -1 # -1 = any + filter_extra_path = "" # text input for adding paths + filter_extra_roots: List[str] = [] # accumulated extra paths + filter_show_private = False + filter_show_macros = False + + # Build unique file list; all enabled by default + all_file_paths: List[str] = sorted(set(e.path.as_posix() for e in entries)) + filter_files_enabled: Dict[str, bool] = {p: True for p in all_file_paths} + + def _rebuild_file_list() -> None: + nonlocal all_file_paths, filter_files_enabled + new_paths = sorted(set(e.path.as_posix() for e in entries)) + old = filter_files_enabled + filter_files_enabled = {p: old.get(p, True) for p in new_paths} + all_file_paths = new_paths + + def _apply_filters(items: List[DocEntry]) -> List[DocEntry]: + result = items + kind = _FILTER_KINDS[filter_kind_idx] + if kind != "all": + result = [e for e in result if e.kind == kind] + # File toggle filter + if not all(filter_files_enabled.get(p, True) for p in all_file_paths): + result = [e for e in result if filter_files_enabled.get(e.path.as_posix(), True)] + # Signature filters + if filter_args >= 0 or filter_returns >= 0: + filtered = [] + for e in result: + n_args, n_rets = _parse_sig_counts(e.stack_effect) + if filter_args >= 0 and n_args != filter_args: + continue + if filter_returns >= 0 and n_rets != filter_returns: + continue + filtered.append(e) + result = filtered + return result while True: - filtered = _filter_docs(entries, query) + filtered = _apply_filters(_filter_docs(entries, query)) if selected >= len(filtered): selected = max(0, len(filtered) - 1) height, width = stdscr.getmaxyx() + if height < 3 or width < 10: + stdscr.erase() + _safe_addnstr(stdscr, 0, 0, "terminal too small", width - 1) + stdscr.refresh() + stdscr.getch() + continue + + # -- DETAIL MODE -- + if mode == _MODE_DETAIL: + stdscr.erase() + _safe_addnstr( + stdscr, 0, 0, + f" {detail_lines[0] if detail_lines else ''} ", + width - 1, curses.A_BOLD, + ) + _safe_addnstr(stdscr, 1, 0, " q/Esc: back j/k/Up/Down: scroll PgUp/PgDn ", width - 1, curses.A_DIM) + body_height = max(1, height - 3) + max_dscroll = max(0, len(detail_lines) - body_height) + if detail_scroll > max_dscroll: + detail_scroll = max_dscroll + for row in range(body_height): + li = detail_scroll + row + if li >= len(detail_lines): + break + _safe_addnstr(stdscr, 2 + row, 0, detail_lines[li], width - 1) + pos_text = f" {detail_scroll + 1}-{min(detail_scroll + body_height, len(detail_lines))}/{len(detail_lines)} " + _safe_addnstr(stdscr, height - 1, 0, pos_text, width - 1, curses.A_DIM) + stdscr.refresh() + key = stdscr.getch() + if key in (27, ord("q"), ord("h"), curses.KEY_LEFT): + mode = _MODE_BROWSE + continue + if key in (curses.KEY_DOWN, ord("j")): + if detail_scroll < max_dscroll: + detail_scroll += 1 + continue + if key in (curses.KEY_UP, ord("k")): + if detail_scroll > 0: + detail_scroll -= 1 + continue + if key == curses.KEY_NPAGE: + detail_scroll = min(max_dscroll, detail_scroll + body_height) + continue + if key == curses.KEY_PPAGE: + detail_scroll = max(0, detail_scroll - body_height) + continue + if key == ord("g"): + detail_scroll = 0 + continue + if key == ord("G"): + detail_scroll = max_dscroll + continue + continue + + # -- FILTER MODE -- + if mode == _MODE_FILTER: + stdscr.erase() + _safe_addnstr(stdscr, 0, 0, " Filters ", width - 1, curses.A_BOLD) + _safe_addnstr(stdscr, 1, 0, " Tab: next field Space/Left/Right: change a: all files n: none Enter/Esc: close ", width - 1, curses.A_DIM) + + _N_FILTER_FIELDS = 7 # kind, args, returns, show_private, show_macros, extra_path, files + row_y = 3 + + # Kind row + kind_label = f" Kind: < {_FILTER_KINDS[filter_kind_idx]:6} >" + kind_attr = curses.A_REVERSE if filter_field == 0 else 0 + _safe_addnstr(stdscr, row_y, 0, kind_label, width - 1, kind_attr) + row_y += 1 + + # Args row + args_val = "any" if filter_args < 0 else str(filter_args) + args_label = f" Args: < {args_val:6} >" + args_attr = curses.A_REVERSE if filter_field == 1 else 0 + _safe_addnstr(stdscr, row_y, 0, args_label, width - 1, args_attr) + row_y += 1 + + # Returns row + rets_val = "any" if filter_returns < 0 else str(filter_returns) + rets_label = f" Rets: < {rets_val:6} >" + rets_attr = curses.A_REVERSE if filter_field == 2 else 0 + _safe_addnstr(stdscr, row_y, 0, rets_label, width - 1, rets_attr) + row_y += 1 + + # Show private row + priv_val = "yes" if filter_show_private else "no" + priv_label = f" Private: < {priv_val:6} >" + priv_attr = curses.A_REVERSE if filter_field == 3 else 0 + _safe_addnstr(stdscr, row_y, 0, priv_label, width - 1, priv_attr) + row_y += 1 + + # Show macros row + macro_val = "yes" if filter_show_macros else "no" + macro_label = f" Macros: < {macro_val:6} >" + macro_attr = curses.A_REVERSE if filter_field == 4 else 0 + _safe_addnstr(stdscr, row_y, 0, macro_label, width - 1, macro_attr) + row_y += 1 + + # Extra path row + if filter_field == 5: + ep_label = f" Path: {filter_extra_path}_" + ep_attr = curses.A_REVERSE + else: + ep_label = f" Path: {filter_extra_path or '(type path, Enter to add)'}" + ep_attr = 0 + _safe_addnstr(stdscr, row_y, 0, ep_label, width - 1, ep_attr) + row_y += 1 + for er in filter_extra_roots: + _safe_addnstr(stdscr, row_y, 0, f" + {er}", width - 1, curses.A_DIM) + row_y += 1 + row_y += 1 + + # Files section + files_header = " Files:" + files_header_attr = curses.A_BOLD if filter_field == 6 else curses.A_DIM + _safe_addnstr(stdscr, row_y, 0, files_header, width - 1, files_header_attr) + row_y += 1 + + file_area_top = row_y + file_area_height = max(1, height - file_area_top - 2) + n_files = len(all_file_paths) + + if filter_field == 6: + # Clamp cursor and scroll + if filter_file_cursor >= n_files: + filter_file_cursor = max(0, n_files - 1) + if filter_file_cursor < filter_file_scroll: + filter_file_scroll = filter_file_cursor + if filter_file_cursor >= filter_file_scroll + file_area_height: + filter_file_scroll = filter_file_cursor - file_area_height + 1 + max_fscroll = max(0, n_files - file_area_height) + if filter_file_scroll > max_fscroll: + filter_file_scroll = max_fscroll + + for row in range(file_area_height): + fi = filter_file_scroll + row + if fi >= n_files: + break + fp = all_file_paths[fi] + mark = "[x]" if filter_files_enabled.get(fp, True) else "[ ]" + label = f" {mark} {fp}" + attr = curses.A_REVERSE if (filter_field == 6 and fi == filter_file_cursor) else 0 + _safe_addnstr(stdscr, file_area_top + row, 0, label, width - 1, attr) + + enabled_count = sum(1 for v in filter_files_enabled.values() if v) + preview = _apply_filters(_filter_docs(entries, query)) + status = f" {enabled_count}/{n_files} files kind={_FILTER_KINDS[filter_kind_idx]} args={args_val} rets={rets_val} {len(preview)} matches " + _safe_addnstr(stdscr, height - 1, 0, status, width - 1, curses.A_DIM) + stdscr.refresh() + key = stdscr.getch() + if key == 27: + mode = _MODE_BROWSE + selected = 0 + scroll = 0 + continue + if key in (10, 13, curses.KEY_ENTER) and filter_field != 5: + mode = _MODE_BROWSE + selected = 0 + scroll = 0 + continue + if key == 9: # Tab + filter_field = (filter_field + 1) % _N_FILTER_FIELDS + continue + if filter_field == 0: + # Kind field + if key in (curses.KEY_LEFT, ord("h")): + filter_kind_idx = (filter_kind_idx - 1) % len(_FILTER_KINDS) + continue + if key in (curses.KEY_RIGHT, ord("l"), ord(" ")): + filter_kind_idx = (filter_kind_idx + 1) % len(_FILTER_KINDS) + continue + elif filter_field == 1: + # Args field: Left/Right to adjust, -1 = any + if key in (curses.KEY_RIGHT, ord("l"), ord(" ")): + filter_args += 1 + if filter_args > 10: + filter_args = -1 + continue + if key in (curses.KEY_LEFT, ord("h")): + filter_args -= 1 + if filter_args < -1: + filter_args = 10 + continue + elif filter_field == 2: + # Returns field: Left/Right to adjust + if key in (curses.KEY_RIGHT, ord("l"), ord(" ")): + filter_returns += 1 + if filter_returns > 10: + filter_returns = -1 + continue + if key in (curses.KEY_LEFT, ord("h")): + filter_returns -= 1 + if filter_returns < -1: + filter_returns = 10 + continue + elif filter_field == 3: + # Show private toggle + if key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("h"), ord("l"), ord(" ")): + filter_show_private = not filter_show_private + if reload_fn is not None: + entries = reload_fn(include_private=filter_show_private, include_macros=filter_show_macros, extra_roots=filter_extra_roots) + _rebuild_file_list() + continue + elif filter_field == 4: + # Show macros toggle + if key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("h"), ord("l"), ord(" ")): + filter_show_macros = not filter_show_macros + if reload_fn is not None: + entries = reload_fn(include_private=filter_show_private, include_macros=filter_show_macros, extra_roots=filter_extra_roots) + _rebuild_file_list() + continue + elif filter_field == 5: + # Extra path: text input, Enter adds to roots + if key in (10, 13, curses.KEY_ENTER): + if filter_extra_path.strip(): + filter_extra_roots.append(filter_extra_path.strip()) + filter_extra_path = "" + if reload_fn is not None: + entries = reload_fn( + include_private=filter_show_private, + include_macros=filter_show_macros, + extra_roots=filter_extra_roots, + ) + _rebuild_file_list() + continue + if key in (curses.KEY_BACKSPACE, 127, 8): + filter_extra_path = filter_extra_path[:-1] + continue + if 32 <= key <= 126: + filter_extra_path += chr(key) + continue + elif filter_field == 6: + # Files field + if key in (curses.KEY_UP, ord("k")): + if filter_file_cursor > 0: + filter_file_cursor -= 1 + continue + if key in (curses.KEY_DOWN, ord("j")): + if filter_file_cursor + 1 < n_files: + filter_file_cursor += 1 + continue + if key == ord(" "): + if 0 <= filter_file_cursor < n_files: + fp = all_file_paths[filter_file_cursor] + filter_files_enabled[fp] = not filter_files_enabled.get(fp, True) + continue + if key == ord("a"): + for fp in all_file_paths: + filter_files_enabled[fp] = True + continue + if key == ord("n"): + for fp in all_file_paths: + filter_files_enabled[fp] = False + continue + if key == curses.KEY_PPAGE: + filter_file_cursor = max(0, filter_file_cursor - file_area_height) + continue + if key == curses.KEY_NPAGE: + filter_file_cursor = min(max(0, n_files - 1), filter_file_cursor + file_area_height) + continue + continue + + # -- SEARCH MODE -- + if mode == _MODE_SEARCH: + stdscr.erase() + prompt = f"/{search_buf}" + _safe_addnstr(stdscr, 0, 0, prompt, width - 1, curses.A_BOLD) + preview = _apply_filters(_filter_docs(entries, search_buf)) + _safe_addnstr(stdscr, 1, 0, f" {len(preview)} matches (Enter: apply Esc: cancel)", width - 1, curses.A_DIM) + preview_height = max(1, height - 3) + for row in range(min(preview_height, len(preview))): + e = preview[row] + effect = e.stack_effect if e.stack_effect else "(no stack effect)" + line = f" {e.name:24} {effect}" + _safe_addnstr(stdscr, 2 + row, 0, line, width - 1) + stdscr.refresh() + try: + curses.curs_set(1) + except Exception: + pass + key = stdscr.getch() + if key == 27: + # Cancel search, revert + search_buf = query + mode = _MODE_BROWSE + try: + curses.curs_set(0) + except Exception: + pass + continue + if key in (10, 13, curses.KEY_ENTER): + query = search_buf + selected = 0 + scroll = 0 + mode = _MODE_BROWSE + try: + curses.curs_set(0) + except Exception: + pass + continue + if key in (curses.KEY_BACKSPACE, 127, 8): + search_buf = search_buf[:-1] + continue + if 32 <= key <= 126: + search_buf += chr(key) + continue + continue + + # -- BROWSE MODE -- list_height = max(1, height - 4) if selected < scroll: scroll = selected @@ -5988,40 +6498,79 @@ def _run_docs_tui(entries: Sequence[DocEntry], initial_query: str = "") -> int: scroll = max_scroll stdscr.erase() - header = f"L2 docs {len(filtered)}/{len(entries)} filter: {query}" - stdscr.addnstr(0, 0, header, max(1, width - 1), curses.A_BOLD) - stdscr.addnstr( - 1, - 0, - "Type to search · name:/effect:/desc:/path:/kind: · -term excludes · Up/Down j/k · PgUp/PgDn · q/Esc", - max(1, width - 1), - 0, - ) + kind_str = _FILTER_KINDS[filter_kind_idx] + enabled_count = sum(1 for v in filter_files_enabled.values() if v) + filter_info = "" + has_kind_filter = kind_str != "all" + has_file_filter = enabled_count < len(all_file_paths) + has_sig_filter = filter_args >= 0 or filter_returns >= 0 + if has_kind_filter or has_file_filter or has_sig_filter or filter_extra_roots or filter_show_private or filter_show_macros: + parts = [] + if has_kind_filter: + parts.append(f"kind={kind_str}") + if has_file_filter: + parts.append(f"files={enabled_count}/{len(all_file_paths)}") + if filter_args >= 0: + parts.append(f"args={filter_args}") + if filter_returns >= 0: + parts.append(f"rets={filter_returns}") + if filter_show_private: + parts.append("private") + if filter_show_macros: + parts.append("macros") + if filter_extra_roots: + parts.append(f"+{len(filter_extra_roots)} paths") + filter_info = " [" + ", ".join(parts) + "]" + header = f" L2 docs {len(filtered)}/{len(entries)}" + (f" search: {query}" if query else "") + filter_info + _safe_addnstr(stdscr, 0, 0, header, width - 1, curses.A_BOLD) + hint = " / search f filters r reload Enter detail j/k nav q quit" + _safe_addnstr(stdscr, 1, 0, hint, width - 1, curses.A_DIM) for row in range(list_height): idx = scroll + row if idx >= len(filtered): break entry = filtered[idx] - effect = entry.stack_effect if entry.stack_effect else "(no stack effect)" - line = f"{entry.name:24} {effect}" + effect = entry.stack_effect if entry.stack_effect else "" + kind_tag = f"[{entry.kind}]" + line = f" {entry.name:24} {effect:30} {kind_tag}" attr = curses.A_REVERSE if idx == selected else 0 - stdscr.addnstr(2 + row, 0, line, max(1, width - 1), attr) + _safe_addnstr(stdscr, 2 + row, 0, line, width - 1, attr) if filtered: current = filtered[selected] - detail = f"{current.path}:{current.line} [{current.kind}]" + detail = f" {current.path}:{current.line}" if current.description: detail += f" {current.description}" - stdscr.addnstr(height - 1, 0, detail, max(1, width - 1), 0) + _safe_addnstr(stdscr, height - 1, 0, detail, width - 1, curses.A_DIM) else: - stdscr.addnstr(height - 1, 0, "No matches", max(1, width - 1), 0) + _safe_addnstr(stdscr, height - 1, 0, " No matches", width - 1, curses.A_DIM) stdscr.refresh() key = stdscr.getch() if key in (27, ord("q")): return 0 + if key == ord("/"): + search_buf = query + mode = _MODE_SEARCH + continue + if key == ord("f"): + mode = _MODE_FILTER + continue + if key == ord("r"): + if reload_fn is not None: + entries = reload_fn(include_private=filter_show_private, include_macros=filter_show_macros, extra_roots=filter_extra_roots) + _rebuild_file_list() + selected = 0 + scroll = 0 + continue + if key in (10, 13, curses.KEY_ENTER): + if filtered: + detail_lines = _build_detail_lines(filtered[selected], width) + detail_scroll = 0 + mode = _MODE_DETAIL + continue if key in (curses.KEY_UP, ord("k")): if selected > 0: selected -= 1 @@ -6036,15 +6585,12 @@ def _run_docs_tui(entries: Sequence[DocEntry], initial_query: str = "") -> int: if key == curses.KEY_NPAGE: selected = min(max(0, len(filtered) - 1), selected + list_height) continue - if key in (curses.KEY_BACKSPACE, 127, 8): - query = query[:-1] + if key == ord("g"): selected = 0 scroll = 0 continue - if 32 <= key <= 126: - query += chr(key) - selected = 0 - scroll = 0 + if key == ord("G"): + selected = max(0, len(filtered) - 1) continue return 0 @@ -6069,13 +6615,46 @@ def run_docs_explorer( roots.append(source.parent) roots.append(source) - entries = collect_docs( - roots, + collect_opts: Dict[str, Any] = dict( include_undocumented=include_undocumented, include_private=include_private, include_tests=include_tests, + include_macros=False, ) - return _run_docs_tui(entries, initial_query=initial_query) + + def _reload(**overrides: Any) -> List[DocEntry]: + extra = overrides.pop("extra_roots", []) + opts = {**collect_opts, **overrides} + entries = collect_docs(roots, **opts) + # Scan extra roots directly, bypassing _iter_doc_files skip filters + # Always include undocumented entries from user-added paths + if extra: + seen_names = {e.name for e in entries} + scan_opts = dict( + include_undocumented=True, + include_private=True, + include_macros=opts.get("include_macros", False), + ) + for p in extra: + ep = Path(p).expanduser().resolve() + if not ep.exists(): + continue + if ep.is_file() and ep.suffix == ".sl": + for e in _scan_doc_file(ep, **scan_opts): + if e.name not in seen_names: + seen_names.add(e.name) + entries.append(e) + elif ep.is_dir(): + for sl in sorted(ep.rglob("*.sl")): + for e in _scan_doc_file(sl.resolve(), **scan_opts): + if e.name not in seen_names: + seen_names.add(e.name) + entries.append(e) + entries.sort(key=lambda item: (item.name.lower(), str(item.path), item.line)) + return entries + + entries = _reload() + return _run_docs_tui(entries, initial_query=initial_query, reload_fn=_reload) def cli(argv: Sequence[str]) -> int: @@ -6213,6 +6792,10 @@ def cli(argv: Sequence[str]) -> int: entry_mode = "program" if artifact_kind == "exe" else "library" emission = compiler.compile_file(args.source, debug=args.debug, entry_mode=entry_mode) + # Snapshot assembly text *before* ct-run-main JIT execution, which may + # corrupt Python heap objects depending on memory layout. + asm_text = emission.snapshot() + if args.ct_run_main: try: compiler.run_compile_time_word("main", libs=args.libs) @@ -6229,7 +6812,7 @@ def cli(argv: Sequence[str]) -> int: args.temp_dir.mkdir(parents=True, exist_ok=True) asm_path = args.temp_dir / (args.source.stem + ".asm") obj_path = args.temp_dir / (args.source.stem + ".o") - compiler.assembler.write_asm(emission, asm_path) + asm_path.write_text(asm_text) if args.emit_asm: print(f"[info] wrote {asm_path}") diff --git a/stdlib/arr.sl b/stdlib/arr.sl index 6695486..981375b 100644 --- a/stdlib/arr.sl +++ b/stdlib/arr.sl @@ -56,10 +56,10 @@ word arr_copy_elements drop 2drop end -#arr_reserve [*, cap | arr] -> [* | arr] +#arr_reserve [*, arr | cap] -> [* | arr] # Ensures capacity >= cap; returns (possibly moved) arr pointer. word arr_reserve - swap dup 1 < if drop 1 end swap # reqcap arr + dup 1 < if drop 1 end swap # stack: [*, reqcap | arr] # Check: if arr_cap >= reqcap, do nothing over over arr_cap swap @@ -86,13 +86,13 @@ word arr_reserve end end -#arr_push [*, x | arr] -> [* | arr] +#arr_push [*, arr | x] -> [* | arr] # Push element onto array, growing if needed word arr_push + swap dup arr_len over arr_cap >= if dup arr_cap dup 1 < if drop 1 end 2 * - over arr_reserve - nip + arr_reserve end # Store x at data[len] @@ -116,16 +116,16 @@ word arr_pop end end -#arr_get [*, i | arr] -> [* | x] +#arr_get [*, arr | i] -> [* | x] # Get element at index i word arr_get - arr_data swap 8 * + @ + swap arr_data swap 8 * + @ end -#arr_set [*, x, i | arr] -> [*] +#arr_set [*, arr, x | i] -> [*] # Set element at index i to x word arr_set - arr_data swap 8 * + swap ! + rot arr_data swap 8 * + swap ! end #dyn_arr_clone [* | dyn_arr] -> [* | dyn_arr_copy] @@ -149,16 +149,16 @@ word arr_item_ptr swap 8 * swap 8 + + end -#arr_get [*, i | arr] -> [* | x] +#arr_get_static [*, arr | i] -> [* | x] # Get element from built-in static array word arr_get_static - arr_item_ptr @ + swap arr_item_ptr @ end -#arr_set [*, x, i | arr] -> [*] +#arr_set_static [*, arr, x | i] -> [*] # Set element in built-in static array word arr_set_static - arr_item_ptr swap ! + rot arr_item_ptr swap ! end #arr_static_free [* | arr] -> [*] diff --git a/tests/arr_dynamic.sl b/tests/arr_dynamic.sl index 721c4a7..ac31089 100644 --- a/tests/arr_dynamic.sl +++ b/tests/arr_dynamic.sl @@ -11,30 +11,30 @@ word main dup arr_data over 24 + == puti cr # arr_push - 10 swap arr_push - 20 swap arr_push - 30 swap arr_push + dup 10 arr_push + dup 20 arr_push + dup 30 arr_push # arr_len / arr_cap after growth dup arr_len puti cr dup arr_cap puti cr # arr_get - dup 0 swap arr_get puti cr - dup 1 swap arr_get puti cr - dup 2 swap arr_get puti cr + dup 0 arr_get puti cr + dup 1 arr_get puti cr + dup 2 arr_get puti cr # arr_set - dup 99 swap 1 swap arr_set - dup 1 swap arr_get puti cr + dup 99 1 arr_set + dup 1 arr_get puti cr # arr_reserve (with len > 0 so element copy path is exercised) - 8 swap arr_reserve + dup 8 arr_reserve dup arr_cap puti cr dup arr_len puti cr - dup 0 swap arr_get puti cr - dup 1 swap arr_get puti cr - dup 2 swap arr_get puti cr + dup 0 arr_get puti cr + dup 1 arr_get puti cr + dup 2 arr_get puti cr # arr_pop (including empty pop) arr_pop puti cr @@ -49,9 +49,9 @@ word main [ 7 8 9 ] dup arr_to_dyn dup arr_len puti cr dup arr_cap puti cr - dup 0 swap arr_get puti cr - dup 1 swap arr_get puti cr - dup 2 swap arr_get puti cr + dup 0 arr_get puti cr + dup 1 arr_get puti cr + dup 2 arr_get puti cr arr_free # free list allocation: bytes = (len + 1) * 8 @@ -59,41 +59,41 @@ word main # dyn_arr_sorted (copy) should not mutate source 5 arr_new - 3 swap arr_push - 1 swap arr_push - 2 swap arr_push + dup 3 arr_push + dup 1 arr_push + dup 2 arr_push dup dyn_arr_sorted - dup 0 swap arr_get puti cr - dup 1 swap arr_get puti cr - dup 2 swap arr_get puti cr + dup 0 arr_get puti cr + dup 1 arr_get puti cr + dup 2 arr_get puti cr arr_free - dup 0 swap arr_get puti cr - dup 1 swap arr_get puti cr - dup 2 swap arr_get puti cr + dup 0 arr_get puti cr + dup 1 arr_get puti cr + dup 2 arr_get puti cr # dyn_arr_sort (alias) sorts in place dyn_arr_sort - dup 0 swap arr_get puti cr - dup 1 swap arr_get puti cr - dup 2 swap arr_get puti cr + dup 0 arr_get puti cr + dup 1 arr_get puti cr + dup 2 arr_get puti cr arr_free # dyn_arr_sorted (alias) returns a sorted copy 5 arr_new - 4 swap arr_push - 9 swap arr_push - 6 swap arr_push + dup 4 arr_push + dup 9 arr_push + dup 6 arr_push dup dyn_arr_sorted - dup 0 swap arr_get puti cr - dup 1 swap arr_get puti cr - dup 2 swap arr_get puti cr + dup 0 arr_get puti cr + dup 1 arr_get puti cr + dup 2 arr_get puti cr arr_free - dup 0 swap arr_get puti cr - dup 1 swap arr_get puti cr - dup 2 swap arr_get puti cr + dup 0 arr_get puti cr + dup 1 arr_get puti cr + dup 2 arr_get puti cr arr_free end diff --git a/tests/arr_static_sort.sl b/tests/arr_static_sort.sl index 610b96f..badd63a 100644 --- a/tests/arr_static_sort.sl +++ b/tests/arr_static_sort.sl @@ -4,21 +4,21 @@ import ../stdlib/arr.sl word main [ 4 1 3 2 ] dup arr_sort - dup 0 swap arr_get_static puti cr - dup 1 swap arr_get_static puti cr - dup 2 swap arr_get_static puti cr - dup 3 swap arr_get_static puti cr + dup 0 arr_get_static puti cr + dup 1 arr_get_static puti cr + dup 2 arr_get_static puti cr + dup 3 arr_get_static puti cr arr_static_free [ 9 5 7 ] dup arr_sorted - dup 0 swap arr_get_static puti cr - dup 1 swap arr_get_static puti cr - dup 2 swap arr_get_static puti cr + dup 0 arr_get_static puti cr + dup 1 arr_get_static puti cr + dup 2 arr_get_static puti cr swap - dup 0 swap arr_get_static puti cr - dup 1 swap arr_get_static puti cr - dup 2 swap arr_get_static puti cr + dup 0 arr_get_static puti cr + dup 1 arr_get_static puti cr + dup 2 arr_get_static puti cr arr_static_free arr_static_free