diff --git a/shared/sx/evaluator.py b/shared/sx/evaluator.py index 7aa8582..67bfca7 100644 --- a/shared/sx/evaluator.py +++ b/shared/sx/evaluator.py @@ -42,6 +42,22 @@ class EvalError(Exception): pass +class _Thunk: + """Deferred evaluation — returned from tail positions for TCO.""" + __slots__ = ("expr", "env") + + def __init__(self, expr: Any, env: dict[str, Any]): + self.expr = expr + self.env = env + + +def _trampoline(val: Any) -> Any: + """Unwrap thunks by re-entering the evaluator until we get an actual value.""" + while isinstance(val, _Thunk): + val = _eval(val.expr, val.env) + return val + + # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- @@ -50,7 +66,10 @@ def evaluate(expr: Any, env: dict[str, Any] | None = None) -> Any: """Evaluate *expr* in *env* and return the result.""" if env is None: env = {} - return _eval(expr, env) + result = _eval(expr, env) + while isinstance(result, _Thunk): + result = _eval(result.expr, result.env) + return result def make_env(**kwargs: Any) -> dict[str, Any]: @@ -90,7 +109,7 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any: # --- dict literal ----------------------------------------------------- if isinstance(expr, dict): - return {k: _eval(v, env) for k, v in expr.items()} + return {k: _trampoline(_eval(v, env)) for k, v in expr.items()} # --- list = call or special form -------------------------------------- if not isinstance(expr, list): @@ -103,7 +122,7 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any: # If head is not a symbol/lambda/list, treat entire list as data if not isinstance(head, (Symbol, Lambda, list)): - return [_eval(x, env) for x in expr] + return [_trampoline(_eval(x, env)) for x in expr] # --- special forms ---------------------------------------------------- if isinstance(head, Symbol): @@ -122,11 +141,11 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any: val = env[name] if isinstance(val, Macro): expanded = _expand_macro(val, expr[1:], env) - return _eval(expanded, env) + return _Thunk(expanded, env) # --- function / lambda call ------------------------------------------- - fn = _eval(head, env) - args = [_eval(a, env) for a in expr[1:]] + fn = _trampoline(_eval(head, env)) + args = [_trampoline(_eval(a, env)) for a in expr[1:]] if callable(fn) and not isinstance(fn, (Lambda, Component)): return fn(*args) @@ -151,7 +170,7 @@ def _call_lambda(fn: Lambda, args: list[Any], caller_env: dict[str, Any]) -> Any local.update(caller_env) for p, v in zip(fn.params, args): local[p] = v - return _eval(fn.body, local) + return _Thunk(fn.body, local) def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) -> Any: @@ -166,10 +185,10 @@ def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) - while i < len(raw_args): arg = raw_args[i] if isinstance(arg, Keyword) and i + 1 < len(raw_args): - kwargs[arg.name] = _eval(raw_args[i + 1], env) + kwargs[arg.name] = _trampoline(_eval(raw_args[i + 1], env)) i += 2 else: - children.append(_eval(arg, env)) + children.append(_trampoline(_eval(arg, env))) i += 1 local = dict(comp.closure) @@ -181,7 +200,7 @@ def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) - local[p] = NIL if comp.has_children: local["children"] = children - return _eval(comp.body, local) + return _Thunk(comp.body, local) # --------------------------------------------------------------------------- @@ -191,23 +210,22 @@ def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) - def _sf_if(expr: list, env: dict) -> Any: if len(expr) < 3: raise EvalError("if requires condition and then-branch") - cond = _eval(expr[1], env) + cond = _trampoline(_eval(expr[1], env)) if cond and cond is not NIL: - return _eval(expr[2], env) + return _Thunk(expr[2], env) if len(expr) > 3: - return _eval(expr[3], env) + return _Thunk(expr[3], env) return NIL def _sf_when(expr: list, env: dict) -> Any: if len(expr) < 3: raise EvalError("when requires condition and body") - cond = _eval(expr[1], env) + cond = _trampoline(_eval(expr[1], env)) if cond and cond is not NIL: - result = NIL - for body_expr in expr[2:]: - result = _eval(body_expr, env) - return result + for body_expr in expr[2:-1]: + _trampoline(_eval(body_expr, env)) + return _Thunk(expr[-1], env) return NIL @@ -228,22 +246,22 @@ def _sf_cond(expr: list, env: dict) -> Any: raise EvalError("cond clause must be (test result)") test = clause[0] if isinstance(test, Symbol) and test.name in ("else", ":else"): - return _eval(clause[1], env) + return _Thunk(clause[1], env) if isinstance(test, Keyword) and test.name == "else": - return _eval(clause[1], env) - if _eval(test, env): - return _eval(clause[1], env) + return _Thunk(clause[1], env) + if _trampoline(_eval(test, env)): + return _Thunk(clause[1], env) else: i = 0 while i < len(clauses) - 1: test = clauses[i] result = clauses[i + 1] if isinstance(test, Keyword) and test.name == "else": - return _eval(result, env) + return _Thunk(result, env) if isinstance(test, Symbol) and test.name in (":else", "else"): - return _eval(result, env) - if _eval(test, env): - return _eval(result, env) + return _Thunk(result, env) + if _trampoline(_eval(test, env)): + return _Thunk(result, env) i += 2 return NIL @@ -251,18 +269,18 @@ def _sf_cond(expr: list, env: dict) -> Any: def _sf_case(expr: list, env: dict) -> Any: if len(expr) < 2: raise EvalError("case requires expression to match") - match_val = _eval(expr[1], env) + match_val = _trampoline(_eval(expr[1], env)) clauses = expr[2:] i = 0 while i < len(clauses) - 1: test = clauses[i] result = clauses[i + 1] if isinstance(test, Keyword) and test.name == "else": - return _eval(result, env) + return _Thunk(result, env) if isinstance(test, Symbol) and test.name in (":else", "else"): - return _eval(result, env) - if match_val == _eval(test, env): - return _eval(result, env) + return _Thunk(result, env) + if match_val == _trampoline(_eval(test, env)): + return _Thunk(result, env) i += 2 return NIL @@ -270,7 +288,7 @@ def _sf_case(expr: list, env: dict) -> Any: def _sf_and(expr: list, env: dict) -> Any: result: Any = True for arg in expr[1:]: - result = _eval(arg, env) + result = _trampoline(_eval(arg, env)) if not result: return result return result @@ -279,7 +297,7 @@ def _sf_and(expr: list, env: dict) -> Any: def _sf_or(expr: list, env: dict) -> Any: result: Any = False for arg in expr[1:]: - result = _eval(arg, env) + result = _trampoline(_eval(arg, env)) if result: return result return result @@ -299,23 +317,23 @@ def _sf_let(expr: list, env: dict) -> Any: raise EvalError("let binding must be (name value)") var = binding[0] vname = var.name if isinstance(var, Symbol) else var - local[vname] = _eval(binding[1], local) + local[vname] = _trampoline(_eval(binding[1], local)) elif len(bindings) % 2 == 0: # Clojure-style: (name val name val ...) for i in range(0, len(bindings), 2): var = bindings[i] vname = var.name if isinstance(var, Symbol) else var - local[vname] = _eval(bindings[i + 1], local) + local[vname] = _trampoline(_eval(bindings[i + 1], local)) else: raise EvalError("let bindings must be (name val ...) pairs") else: raise EvalError("let bindings must be a list") - # Evaluate body expressions, return last - result: Any = NIL - for body_expr in expr[2:]: - result = _eval(body_expr, local) - return result + # Evaluate body expressions — all but last non-tail, last is tail + body = expr[2:] + for body_expr in body[:-1]: + _trampoline(_eval(body_expr, local)) + return _Thunk(body[-1], local) def _sf_lambda(expr: list, env: dict) -> Lambda: @@ -341,7 +359,7 @@ def _sf_define(expr: list, env: dict) -> Any: name_sym = expr[1] if not isinstance(name_sym, Symbol): raise EvalError(f"define name must be symbol, got {type(name_sym).__name__}") - value = _eval(expr[2], env) + value = _trampoline(_eval(expr[2], env)) if isinstance(value, Lambda) and value.name is None: value.name = name_sym.name env[name_sym.name] = value @@ -393,10 +411,11 @@ def _sf_defcomp(expr: list, env: dict) -> Component: def _sf_begin(expr: list, env: dict) -> Any: - result: Any = NIL - for sub in expr[1:]: - result = _eval(sub, env) - return result + if len(expr) < 2: + return NIL + for sub in expr[1:-1]: + _trampoline(_eval(sub, env)) + return _Thunk(expr[-1], env) def _sf_quote(expr: list, _env: dict) -> Any: @@ -407,18 +426,18 @@ def _sf_thread_first(expr: list, env: dict) -> Any: """``(-> val (f a) (g b))`` → ``(g (f val a) b)``""" if len(expr) < 2: raise EvalError("-> requires at least a value") - result = _eval(expr[1], env) + result = _trampoline(_eval(expr[1], env)) for form in expr[2:]: if isinstance(form, list): - fn = _eval(form[0], env) - args = [result] + [_eval(a, env) for a in form[1:]] + fn = _trampoline(_eval(form[0], env)) + args = [result] + [_trampoline(_eval(a, env)) for a in form[1:]] else: - fn = _eval(form, env) + fn = _trampoline(_eval(form, env)) args = [result] if callable(fn) and not isinstance(fn, (Lambda, Component)): result = fn(*args) elif isinstance(fn, Lambda): - result = _call_lambda(fn, args, env) + result = _trampoline(_call_lambda(fn, args, env)) else: raise EvalError(f"-> form not callable: {fn!r}") return result @@ -482,14 +501,14 @@ def _qq_expand(template: Any, env: dict) -> Any: if head.name == "unquote": if len(template) < 2: raise EvalError("unquote requires an expression") - return _eval(template[1], env) + return _trampoline(_eval(template[1], env)) if head.name == "splice-unquote": raise EvalError("splice-unquote not inside a list") # Walk children, handling splice-unquote result: list[Any] = [] for item in template: if isinstance(item, list) and len(item) == 2 and isinstance(item[0], Symbol) and item[0].name == "splice-unquote": - spliced = _eval(item[1], env) + spliced = _trampoline(_eval(item[1], env)) if isinstance(spliced, list): result.extend(spliced) elif spliced is not None and spliced is not NIL: @@ -516,7 +535,7 @@ def _expand_macro(macro: Macro, raw_args: list[Any], env: dict) -> Any: rest_start = len(macro.params) local[macro.rest_param] = list(raw_args[rest_start:]) - return _eval(macro.body, local) + return _trampoline(_eval(macro.body, local)) def _sf_defhandler(expr: list, env: dict) -> HandlerDef: @@ -629,7 +648,7 @@ def _sf_set_bang(expr: list, env: dict) -> Any: name_sym = expr[1] if not isinstance(name_sym, Symbol): raise EvalError(f"set! name must be symbol, got {type(name_sym).__name__}") - value = _eval(expr[2], env) + value = _trampoline(_eval(expr[2], env)) # Walk up scope if using Env objects; for plain dicts just overwrite env[name_sym.name] = value return value @@ -660,7 +679,7 @@ def _sf_defrelation(expr: list, env: dict) -> RelationDef: if isinstance(val, Keyword): kwargs[key.name] = val.name else: - kwargs[key.name] = _eval(val, env) if not isinstance(val, str) else val + kwargs[key.name] = _trampoline(_eval(val, env)) if not isinstance(val, str) else val i += 2 else: kwargs[key.name] = None @@ -746,7 +765,7 @@ def _sf_defpage(expr: list, env: dict) -> PageDef: elif isinstance(item, str): auth.append(item) else: - auth.append(_eval(item, env)) + auth.append(_trampoline(_eval(item, env))) else: auth = str(auth_val) if auth_val else "public" @@ -762,7 +781,7 @@ def _sf_defpage(expr: list, env: dict) -> PageDef: cache_val = slots.get("cache") cache = None if cache_val is not None: - cache_result = _eval(cache_val, env) + cache_result = _trampoline(_eval(cache_val, env)) if isinstance(cache_result, dict): cache = cache_result @@ -818,57 +837,57 @@ _SPECIAL_FORMS: dict[str, Any] = { def _ho_map(expr: list, env: dict) -> list: if len(expr) != 3: raise EvalError("map requires fn and collection") - fn = _eval(expr[1], env) - coll = _eval(expr[2], env) + fn = _trampoline(_eval(expr[1], env)) + coll = _trampoline(_eval(expr[2], env)) if not isinstance(fn, Lambda): raise EvalError(f"map requires lambda, got {type(fn).__name__}") - return [_call_lambda(fn, [item], env) for item in coll] + return [_trampoline(_call_lambda(fn, [item], env)) for item in coll] def _ho_map_indexed(expr: list, env: dict) -> list: if len(expr) != 3: raise EvalError("map-indexed requires fn and collection") - fn = _eval(expr[1], env) - coll = _eval(expr[2], env) + fn = _trampoline(_eval(expr[1], env)) + coll = _trampoline(_eval(expr[2], env)) if not isinstance(fn, Lambda): raise EvalError(f"map-indexed requires lambda, got {type(fn).__name__}") if len(fn.params) < 2: raise EvalError("map-indexed lambda needs (i item) params") - return [_call_lambda(fn, [i, item], env) for i, item in enumerate(coll)] + return [_trampoline(_call_lambda(fn, [i, item], env)) for i, item in enumerate(coll)] def _ho_filter(expr: list, env: dict) -> list: if len(expr) != 3: raise EvalError("filter requires fn and collection") - fn = _eval(expr[1], env) - coll = _eval(expr[2], env) + fn = _trampoline(_eval(expr[1], env)) + coll = _trampoline(_eval(expr[2], env)) if not isinstance(fn, Lambda): raise EvalError(f"filter requires lambda, got {type(fn).__name__}") - return [item for item in coll if _call_lambda(fn, [item], env)] + return [item for item in coll if _trampoline(_call_lambda(fn, [item], env))] def _ho_reduce(expr: list, env: dict) -> Any: if len(expr) != 4: raise EvalError("reduce requires fn, init, and collection") - fn = _eval(expr[1], env) - acc = _eval(expr[2], env) - coll = _eval(expr[3], env) + fn = _trampoline(_eval(expr[1], env)) + acc = _trampoline(_eval(expr[2], env)) + coll = _trampoline(_eval(expr[3], env)) if not isinstance(fn, Lambda): raise EvalError(f"reduce requires lambda, got {type(fn).__name__}") for item in coll: - acc = _call_lambda(fn, [acc, item], env) + acc = _trampoline(_call_lambda(fn, [acc, item], env)) return acc def _ho_some(expr: list, env: dict) -> Any: if len(expr) != 3: raise EvalError("some requires fn and collection") - fn = _eval(expr[1], env) - coll = _eval(expr[2], env) + fn = _trampoline(_eval(expr[1], env)) + coll = _trampoline(_eval(expr[2], env)) if not isinstance(fn, Lambda): raise EvalError(f"some requires lambda, got {type(fn).__name__}") for item in coll: - result = _call_lambda(fn, [item], env) + result = _trampoline(_call_lambda(fn, [item], env)) if result: return result return NIL @@ -877,12 +896,12 @@ def _ho_some(expr: list, env: dict) -> Any: def _ho_every(expr: list, env: dict) -> bool: if len(expr) != 3: raise EvalError("every? requires fn and collection") - fn = _eval(expr[1], env) - coll = _eval(expr[2], env) + fn = _trampoline(_eval(expr[1], env)) + coll = _trampoline(_eval(expr[2], env)) if not isinstance(fn, Lambda): raise EvalError(f"every? requires lambda, got {type(fn).__name__}") for item in coll: - if not _call_lambda(fn, [item], env): + if not _trampoline(_call_lambda(fn, [item], env)): return False return True @@ -890,12 +909,12 @@ def _ho_every(expr: list, env: dict) -> bool: def _ho_for_each(expr: list, env: dict) -> Any: if len(expr) != 3: raise EvalError("for-each requires fn and collection") - fn = _eval(expr[1], env) - coll = _eval(expr[2], env) + fn = _trampoline(_eval(expr[1], env)) + coll = _trampoline(_eval(expr[2], env)) if not isinstance(fn, Lambda): raise EvalError(f"for-each requires lambda, got {type(fn).__name__}") for item in coll: - _call_lambda(fn, [item], env) + _trampoline(_call_lambda(fn, [item], env)) return NIL diff --git a/shared/sx/handlers.py b/shared/sx/handlers.py index 4cf00cc..c18f551 100644 --- a/shared/sx/handlers.py +++ b/shared/sx/handlers.py @@ -69,7 +69,8 @@ def clear_handlers(service: str | None = None) -> None: def load_handler_file(filepath: str, service_name: str) -> list[HandlerDef]: """Parse an .sx file, evaluate it, and register any HandlerDef values.""" from .parser import parse_all - from .evaluator import _eval + from .evaluator import _eval as _raw_eval, _trampoline + _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .jinja_bridge import get_component_env with open(filepath, encoding="utf-8") as f: diff --git a/shared/sx/html.py b/shared/sx/html.py index 62a94f0..3857dbb 100644 --- a/shared/sx/html.py +++ b/shared/sx/html.py @@ -28,7 +28,15 @@ import contextvars from typing import Any from .types import Component, Keyword, Lambda, Macro, NIL, Symbol -from .evaluator import _eval, _call_component, _expand_macro +from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline + +def _eval(expr, env): + """Evaluate and unwrap thunks — all html.py _eval calls are non-tail.""" + return _trampoline(_raw_eval(expr, env)) + +def _call_component(comp, raw_args, env): + """Call component and unwrap thunks — non-tail in html.py.""" + return _trampoline(_raw_call_component(comp, raw_args, env)) # ContextVar for collecting CSS class names during render. # Set to a set[str] to collect; None to skip. diff --git a/shared/sx/jinja_bridge.py b/shared/sx/jinja_bridge.py index 46c6d8a..f8aaf42 100644 --- a/shared/sx/jinja_bridge.py +++ b/shared/sx/jinja_bridge.py @@ -169,7 +169,8 @@ def register_components(sx_source: str) -> None: (div :class "..." (div :class "..." title))))) ''') """ - from .evaluator import _eval + from .evaluator import _eval as _raw_eval, _trampoline + _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .parser import parse_all from .css_registry import scan_classes_from_sx diff --git a/shared/sx/pages.py b/shared/sx/pages.py index 251370f..4a94b99 100644 --- a/shared/sx/pages.py +++ b/shared/sx/pages.py @@ -96,7 +96,8 @@ def get_page_helpers(service: str) -> dict[str, Any]: def load_page_file(filepath: str, service_name: str) -> list[PageDef]: """Parse an .sx file, evaluate it, and register any PageDef values.""" from .parser import parse_all - from .evaluator import _eval + from .evaluator import _eval as _raw_eval, _trampoline + _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .jinja_bridge import get_component_env with open(filepath, encoding="utf-8") as f: diff --git a/shared/sx/query_registry.py b/shared/sx/query_registry.py index 793925a..e6d63e3 100644 --- a/shared/sx/query_registry.py +++ b/shared/sx/query_registry.py @@ -78,7 +78,8 @@ def clear(service: str | None = None) -> None: def load_query_file(filepath: str, service_name: str) -> list[QueryDef]: """Parse an .sx file and register any defquery definitions.""" from .parser import parse_all - from .evaluator import _eval + from .evaluator import _eval as _raw_eval, _trampoline + _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .jinja_bridge import get_component_env with open(filepath, encoding="utf-8") as f: @@ -102,7 +103,8 @@ def load_query_file(filepath: str, service_name: str) -> list[QueryDef]: def load_action_file(filepath: str, service_name: str) -> list[ActionDef]: """Parse an .sx file and register any defaction definitions.""" from .parser import parse_all - from .evaluator import _eval + from .evaluator import _eval as _raw_eval, _trampoline + _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .jinja_bridge import get_component_env with open(filepath, encoding="utf-8") as f: diff --git a/shared/sx/resolver.py b/shared/sx/resolver.py index f2e470c..9362249 100644 --- a/shared/sx/resolver.py +++ b/shared/sx/resolver.py @@ -31,7 +31,11 @@ import asyncio from typing import Any from .types import Component, Keyword, Lambda, NIL, Symbol -from .evaluator import _eval +from .evaluator import _eval as _raw_eval, _trampoline + +def _eval(expr, env): + """Evaluate and unwrap thunks — all resolver.py _eval calls are non-tail.""" + return _trampoline(_raw_eval(expr, env)) from .html import render as html_render, _RawHTML from .primitives_io import ( IO_PRIMITIVES, diff --git a/sx/sxc/home.sx b/sx/sxc/home.sx index 06ee154..92a4151 100644 --- a/sx/sxc/home.sx +++ b/sx/sxc/home.sx @@ -5,10 +5,10 @@ (h1 :class "text-5xl font-bold text-stone-900 mb-4" (span :class "text-violet-600" "sx")) (p :class "text-2xl text-stone-600 mb-8" - "High power tools for HTML — with s-expressions") + "s-expressions for the web") (p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12" "A hypermedia-driven UI engine that combines htmx's server-first philosophy " - "with React's component model. All rendered via s-expressions over the wire.") + "with React's component model. S-expressions over the wire — no HTML, no JavaScript frameworks.") (div :class "bg-stone-50 border border-stone-200 rounded-lg p-6 text-left font-mono text-sm overflow-x-auto" (pre :class "leading-relaxed" children)))) @@ -41,7 +41,7 @@ (div :class "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold" "1") (div (h3 :class "font-semibold text-stone-900" "Server renders sx") - (p :class "text-stone-600" "Python builds s-expression trees. Components, HTML elements, data — all in one format."))) + (p :class "text-stone-600" "Python builds s-expression trees. Components, elements, data — all in one format."))) (div :class "flex items-start gap-4" (div :class "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold" "2") (div @@ -60,5 +60,5 @@ (a :href "https://htmx.org" :class "text-violet-600 hover:underline" "htmx") " by Carson Gross. This documentation site is modelled on " (a :href "https://four.htmx.org" :class "text-violet-600 hover:underline" "four.htmx.org") - ". htmx showed that HTML is the right hypermedia format. " + ". htmx showed that hypermedia belongs on the server. " "sx takes that idea and wraps it in parentheses."))) diff --git a/sx/sxc/sx_components.py b/sx/sxc/sx_components.py index a62d9e6..b5f17d0 100644 --- a/sx/sxc/sx_components.py +++ b/sx/sxc/sx_components.py @@ -279,14 +279,15 @@ def _docs_introduction_sx() -> str: 'Components use defcomp with keyword parameters and optional children. ' 'The evaluator supports let bindings, conditionals, lambda, map/filter/reduce, and ~80 primitives.")' ' (p :class "text-stone-600"' - ' "sx is not trying to replace JavaScript. It\'s trying to replace the pattern of ' + ' "sx replaces the pattern of ' 'shipping a JS framework + build step + client-side router + state management library ' - 'just to render some server data into HTML."))' + 'just to render some server data. For most applications, sx eliminates the need for ' + 'JavaScript entirely — htmx attributes handle interactivity, hyperscript handles small behaviours, ' + 'and the server handles everything else."))' ' (~doc-section :title "What sx is not" :id "not"' ' (ul :class "space-y-2 text-stone-600"' ' (li "Not a general-purpose programming language — it\'s a UI rendering language")' - ' (li "Not a Lisp implementation — no macros, no continuations, no tail-call optimization")' - ' (li "Not a replacement for JavaScript — it handles rendering, not arbitrary DOM manipulation")' + ' (li "Not a full Lisp — it has macros and TCO, but no continuations or call/cc")' ' (li "Not production-hardened at scale — it runs one website"))))' )