Add types.sx gradual type system spec module with 44 tests

Implements subtype checking, type inference, type narrowing, and
component call-site checking. All type logic is in types.sx (spec),
bootstrapped to every host. Adds test-types.sx with full coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 17:06:09 +00:00
parent 4c4806c8dd
commit e5dbe9f3da
6 changed files with 979 additions and 448 deletions

View File

@@ -307,6 +307,14 @@ def component_affinity(c):
return getattr(c, 'affinity', 'auto')
def component_param_types(c):
return getattr(c, 'param_types', None)
def component_set_param_types(c, d):
c.param_types = d
def macro_params(m):
return m.params
@@ -1428,6 +1436,7 @@ SPEC_MODULES = {
"router": ("router.sx", "router (client-side route matching)"),
"engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"),
"signals": ("signals.sx", "signals (reactive signal runtime)"),
"types": ("types.sx", "types (gradual type system)"),
}
EXTENSION_NAMES = {"continuations"}

View File

@@ -266,6 +266,14 @@ def component_affinity(c):
return getattr(c, 'affinity', 'auto')
def component_param_types(c):
return getattr(c, 'param_types', None)
def component_set_param_types(c, d):
c.param_types = d
def macro_params(m):
return m.params
@@ -2342,454 +2350,6 @@ def env_components(env):
return filter(lambda k: (lambda v: (is_component(v) if sx_truthy(is_component(v)) else is_macro(v)))(env_get(env, k)), keys(env))
# === Transpiled from engine (fetch/swap/trigger pure logic) ===
# ENGINE_VERBS
ENGINE_VERBS = ['get', 'post', 'put', 'delete', 'patch']
# DEFAULT_SWAP
DEFAULT_SWAP = 'outerHTML'
# parse-time
def parse_time(s):
if sx_truthy(is_nil(s)):
return 0
elif sx_truthy(ends_with_p(s, 'ms')):
return parse_int(s, 0)
elif sx_truthy(ends_with_p(s, 's')):
return (parse_int(replace(s, 's', ''), 0) * 1000)
else:
return parse_int(s, 0)
# parse-trigger-spec
def parse_trigger_spec(spec):
if sx_truthy(is_nil(spec)):
return NIL
else:
raw_parts = split(spec, ',')
return filter(lambda x: (not sx_truthy(is_nil(x))), map(lambda part: (lambda tokens: (NIL if sx_truthy(empty_p(tokens)) else ({'event': 'every', 'modifiers': {'interval': parse_time(nth(tokens, 1))}} if sx_truthy(((first(tokens) == 'every') if not sx_truthy((first(tokens) == 'every')) else (len(tokens) >= 2))) else (lambda mods: _sx_begin(for_each(lambda tok: (_sx_dict_set(mods, 'once', True) if sx_truthy((tok == 'once')) else (_sx_dict_set(mods, 'changed', True) if sx_truthy((tok == 'changed')) else (_sx_dict_set(mods, 'delay', parse_time(slice(tok, 6))) if sx_truthy(starts_with_p(tok, 'delay:')) else (_sx_dict_set(mods, 'from', slice(tok, 5)) if sx_truthy(starts_with_p(tok, 'from:')) else NIL)))), rest(tokens)), {'event': first(tokens), 'modifiers': mods}))({}))))(split(trim(part), ' ')), raw_parts))
# default-trigger
def default_trigger(tag_name):
if sx_truthy((tag_name == 'FORM')):
return [{'event': 'submit', 'modifiers': {}}]
elif sx_truthy(((tag_name == 'INPUT') if sx_truthy((tag_name == 'INPUT')) else ((tag_name == 'SELECT') if sx_truthy((tag_name == 'SELECT')) else (tag_name == 'TEXTAREA')))):
return [{'event': 'change', 'modifiers': {}}]
else:
return [{'event': 'click', 'modifiers': {}}]
# get-verb-info
def get_verb_info(el):
return some(lambda verb: (lambda url: ({'method': upper(verb), 'url': url} if sx_truthy(url) else NIL))(dom_get_attr(el, sx_str('sx-', verb))), ENGINE_VERBS)
# build-request-headers
def build_request_headers(el, loaded_components, css_hash):
headers = {'SX-Request': 'true', 'SX-Current-URL': browser_location_href()}
target_sel = dom_get_attr(el, 'sx-target')
if sx_truthy(target_sel):
headers['SX-Target'] = target_sel
if sx_truthy((not sx_truthy(empty_p(loaded_components)))):
headers['SX-Components'] = join(',', loaded_components)
if sx_truthy(css_hash):
headers['SX-Css'] = css_hash
extra_h = dom_get_attr(el, 'sx-headers')
if sx_truthy(extra_h):
parsed = parse_header_value(extra_h)
if sx_truthy(parsed):
for key in keys(parsed):
headers[key] = sx_str(get(parsed, key))
return headers
# process-response-headers
def process_response_headers(get_header):
return {'redirect': get_header('SX-Redirect'), 'refresh': get_header('SX-Refresh'), 'trigger': get_header('SX-Trigger'), 'retarget': get_header('SX-Retarget'), 'reswap': get_header('SX-Reswap'), 'location': get_header('SX-Location'), 'replace-url': get_header('SX-Replace-Url'), 'css-hash': get_header('SX-Css-Hash'), 'trigger-swap': get_header('SX-Trigger-After-Swap'), 'trigger-settle': get_header('SX-Trigger-After-Settle'), 'content-type': get_header('Content-Type'), 'cache-invalidate': get_header('SX-Cache-Invalidate'), 'cache-update': get_header('SX-Cache-Update')}
# parse-swap-spec
def parse_swap_spec(raw_swap, global_transitions_p):
_cells = {}
parts = split((raw_swap if sx_truthy(raw_swap) else DEFAULT_SWAP), ' ')
style = first(parts)
_cells['use_transition'] = global_transitions_p
for p in rest(parts):
if sx_truthy((p == 'transition:true')):
_cells['use_transition'] = True
elif sx_truthy((p == 'transition:false')):
_cells['use_transition'] = False
return {'style': style, 'transition': _cells['use_transition']}
# parse-retry-spec
def parse_retry_spec(retry_attr):
if sx_truthy(is_nil(retry_attr)):
return NIL
else:
parts = split(retry_attr, ':')
return {'strategy': first(parts), 'start-ms': parse_int(nth(parts, 1), 1000), 'cap-ms': parse_int(nth(parts, 2), 30000)}
# next-retry-ms
def next_retry_ms(current_ms, cap_ms):
return min((current_ms * 2), cap_ms)
# filter-params
def filter_params(params_spec, all_params):
if sx_truthy(is_nil(params_spec)):
return all_params
elif sx_truthy((params_spec == 'none')):
return []
elif sx_truthy((params_spec == '*')):
return all_params
elif sx_truthy(starts_with_p(params_spec, 'not ')):
excluded = map(trim, split(slice(params_spec, 4), ','))
return filter(lambda p: (not sx_truthy(contains_p(excluded, first(p)))), all_params)
else:
allowed = map(trim, split(params_spec, ','))
return filter(lambda p: contains_p(allowed, first(p)), all_params)
# resolve-target
def resolve_target(el):
sel = dom_get_attr(el, 'sx-target')
if sx_truthy((is_nil(sel) if sx_truthy(is_nil(sel)) else (sel == 'this'))):
return el
elif sx_truthy((sel == 'closest')):
return dom_parent(el)
else:
return dom_query(sel)
# apply-optimistic
def apply_optimistic(el):
directive = dom_get_attr(el, 'sx-optimistic')
if sx_truthy(is_nil(directive)):
return NIL
else:
target = (resolve_target(el) if sx_truthy(resolve_target(el)) else el)
state = {'target': target, 'directive': directive}
(_sx_begin(_sx_dict_set(state, 'opacity', dom_get_style(target, 'opacity')), dom_set_style(target, 'opacity', '0'), dom_set_style(target, 'pointer-events', 'none')) if sx_truthy((directive == 'remove')) else (_sx_begin(_sx_dict_set(state, 'disabled', dom_get_prop(target, 'disabled')), dom_set_prop(target, 'disabled', True)) if sx_truthy((directive == 'disable')) else ((lambda cls: _sx_begin(_sx_dict_set(state, 'add-class', cls), dom_add_class(target, cls)))(slice(directive, 10)) if sx_truthy(starts_with_p(directive, 'add-class:')) else NIL)))
return state
# revert-optimistic
def revert_optimistic(state):
if sx_truthy(state):
target = get(state, 'target')
directive = get(state, 'directive')
if sx_truthy((directive == 'remove')):
dom_set_style(target, 'opacity', (get(state, 'opacity') if sx_truthy(get(state, 'opacity')) else ''))
return dom_set_style(target, 'pointer-events', '')
elif sx_truthy((directive == 'disable')):
return dom_set_prop(target, 'disabled', (get(state, 'disabled') if sx_truthy(get(state, 'disabled')) else False))
elif sx_truthy(get(state, 'add-class')):
return dom_remove_class(target, get(state, 'add-class'))
return NIL
return NIL
# find-oob-swaps
def find_oob_swaps(container):
results = []
for attr in ['sx-swap-oob', 'hx-swap-oob']:
oob_els = dom_query_all(container, sx_str('[', attr, ']'))
for oob in oob_els:
swap_type = (dom_get_attr(oob, attr) if sx_truthy(dom_get_attr(oob, attr)) else 'outerHTML')
target_id = dom_id(oob)
dom_remove_attr(oob, attr)
if sx_truthy(target_id):
results.append({'element': oob, 'swap-type': swap_type, 'target-id': target_id})
return results
# morph-node
def morph_node(old_node, new_node):
if sx_truthy((dom_has_attr_p(old_node, 'sx-preserve') if sx_truthy(dom_has_attr_p(old_node, 'sx-preserve')) else dom_has_attr_p(old_node, 'sx-ignore'))):
return NIL
elif sx_truthy((dom_has_attr_p(old_node, 'data-sx-island') if not sx_truthy(dom_has_attr_p(old_node, 'data-sx-island')) else (is_processed_p(old_node, 'island-hydrated') if not sx_truthy(is_processed_p(old_node, 'island-hydrated')) else (dom_has_attr_p(new_node, 'data-sx-island') if not sx_truthy(dom_has_attr_p(new_node, 'data-sx-island')) else (dom_get_attr(old_node, 'data-sx-island') == dom_get_attr(new_node, 'data-sx-island')))))):
return morph_island_children(old_node, new_node)
elif sx_truthy(((not sx_truthy((dom_node_type(old_node) == dom_node_type(new_node)))) if sx_truthy((not sx_truthy((dom_node_type(old_node) == dom_node_type(new_node))))) else (not sx_truthy((dom_node_name(old_node) == dom_node_name(new_node)))))):
return dom_replace_child(dom_parent(old_node), dom_clone(new_node), old_node)
elif sx_truthy(((dom_node_type(old_node) == 3) if sx_truthy((dom_node_type(old_node) == 3)) else (dom_node_type(old_node) == 8))):
if sx_truthy((not sx_truthy((dom_text_content(old_node) == dom_text_content(new_node))))):
return dom_set_text_content(old_node, dom_text_content(new_node))
return NIL
elif sx_truthy((dom_node_type(old_node) == 1)):
sync_attrs(old_node, new_node)
if sx_truthy((not sx_truthy((dom_is_active_element_p(old_node) if not sx_truthy(dom_is_active_element_p(old_node)) else dom_is_input_element_p(old_node))))):
return morph_children(old_node, new_node)
return NIL
return NIL
# sync-attrs
def sync_attrs(old_el, new_el):
ra_str = (dom_get_attr(old_el, 'data-sx-reactive-attrs') if sx_truthy(dom_get_attr(old_el, 'data-sx-reactive-attrs')) else '')
reactive_attrs = ([] if sx_truthy(empty_p(ra_str)) else split(ra_str, ','))
for attr in dom_attr_list(new_el):
name = first(attr)
val = nth(attr, 1)
if sx_truthy(((not sx_truthy((dom_get_attr(old_el, name) == val))) if not sx_truthy((not sx_truthy((dom_get_attr(old_el, name) == val)))) else (not sx_truthy(contains_p(reactive_attrs, name))))):
dom_set_attr(old_el, name, val)
return for_each(lambda attr: (lambda aname: (dom_remove_attr(old_el, aname) if sx_truthy(((not sx_truthy(dom_has_attr_p(new_el, aname))) if not sx_truthy((not sx_truthy(dom_has_attr_p(new_el, aname)))) else ((not sx_truthy(contains_p(reactive_attrs, aname))) if not sx_truthy((not sx_truthy(contains_p(reactive_attrs, aname)))) else (not sx_truthy((aname == 'data-sx-reactive-attrs')))))) else NIL))(first(attr)), dom_attr_list(old_el))
# morph-children
def morph_children(old_parent, new_parent):
_cells = {}
old_kids = dom_child_list(old_parent)
new_kids = dom_child_list(new_parent)
old_by_id = reduce(lambda acc, kid: (lambda id_: (_sx_begin(_sx_dict_set(acc, id_, kid), acc) if sx_truthy(id_) else acc))(dom_id(kid)), {}, old_kids)
_cells['oi'] = 0
for new_child in new_kids:
match_id = dom_id(new_child)
match_by_id = (dict_get(old_by_id, match_id) if sx_truthy(match_id) else NIL)
if sx_truthy((match_by_id if not sx_truthy(match_by_id) else (not sx_truthy(is_nil(match_by_id))))):
if sx_truthy(((_cells['oi'] < len(old_kids)) if not sx_truthy((_cells['oi'] < len(old_kids))) else (not sx_truthy((match_by_id == nth(old_kids, _cells['oi'])))))):
dom_insert_before(old_parent, match_by_id, (nth(old_kids, _cells['oi']) if sx_truthy((_cells['oi'] < len(old_kids))) else NIL))
morph_node(match_by_id, new_child)
_cells['oi'] = (_cells['oi'] + 1)
elif sx_truthy((_cells['oi'] < len(old_kids))):
old_child = nth(old_kids, _cells['oi'])
if sx_truthy((dom_id(old_child) if not sx_truthy(dom_id(old_child)) else (not sx_truthy(match_id)))):
dom_insert_before(old_parent, dom_clone(new_child), old_child)
else:
morph_node(old_child, new_child)
_cells['oi'] = (_cells['oi'] + 1)
else:
dom_append(old_parent, dom_clone(new_child))
return for_each(lambda i: ((lambda leftover: (dom_remove_child(old_parent, leftover) if sx_truthy((dom_is_child_of_p(leftover, old_parent) if not sx_truthy(dom_is_child_of_p(leftover, old_parent)) else ((not sx_truthy(dom_has_attr_p(leftover, 'sx-preserve'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(leftover, 'sx-preserve')))) else (not sx_truthy(dom_has_attr_p(leftover, 'sx-ignore')))))) else NIL))(nth(old_kids, i)) if sx_truthy((i >= _cells['oi'])) else NIL), range(_cells['oi'], len(old_kids)))
# morph-island-children
def morph_island_children(old_island, new_island):
old_lakes = dom_query_all(old_island, '[data-sx-lake]')
new_lakes = dom_query_all(new_island, '[data-sx-lake]')
old_marshes = dom_query_all(old_island, '[data-sx-marsh]')
new_marshes = dom_query_all(new_island, '[data-sx-marsh]')
new_lake_map = {}
new_marsh_map = {}
for lake in new_lakes:
id_ = dom_get_attr(lake, 'data-sx-lake')
if sx_truthy(id_):
new_lake_map[id_] = lake
for marsh in new_marshes:
id_ = dom_get_attr(marsh, 'data-sx-marsh')
if sx_truthy(id_):
new_marsh_map[id_] = marsh
for old_lake in old_lakes:
id_ = dom_get_attr(old_lake, 'data-sx-lake')
new_lake = dict_get(new_lake_map, id_)
if sx_truthy(new_lake):
sync_attrs(old_lake, new_lake)
morph_children(old_lake, new_lake)
for old_marsh in old_marshes:
id_ = dom_get_attr(old_marsh, 'data-sx-marsh')
new_marsh = dict_get(new_marsh_map, id_)
if sx_truthy(new_marsh):
morph_marsh(old_marsh, new_marsh, old_island)
return process_signal_updates(new_island)
# morph-marsh
def morph_marsh(old_marsh, new_marsh, island_el):
transform = dom_get_data(old_marsh, 'sx-marsh-transform')
env = dom_get_data(old_marsh, 'sx-marsh-env')
new_html = dom_inner_html(new_marsh)
if sx_truthy((env if not sx_truthy(env) else (new_html if not sx_truthy(new_html) else (not sx_truthy(empty_p(new_html)))))):
parsed = parse(new_html)
sx_content = (invoke(transform, parsed) if sx_truthy(transform) else parsed)
dispose_marsh_scope(old_marsh)
return with_marsh_scope(old_marsh, lambda : (lambda new_dom: _sx_begin(dom_remove_children_after(old_marsh, NIL), dom_append(old_marsh, new_dom)))(render_to_dom(sx_content, env, NIL)))
else:
sync_attrs(old_marsh, new_marsh)
return morph_children(old_marsh, new_marsh)
# process-signal-updates
def process_signal_updates(root):
signal_els = dom_query_all(root, '[data-sx-signal]')
return for_each(lambda el: (lambda spec: ((lambda colon_idx: ((lambda store_name: (lambda raw_value: _sx_begin((lambda parsed: reset_b(use_store(store_name), parsed))(json_parse(raw_value)), dom_remove_attr(el, 'data-sx-signal')))(slice(spec, (colon_idx + 1))))(slice(spec, 0, colon_idx)) if sx_truthy((colon_idx > 0)) else NIL))(index_of(spec, ':')) if sx_truthy(spec) else NIL))(dom_get_attr(el, 'data-sx-signal')), signal_els)
# swap-dom-nodes
def swap_dom_nodes(target, new_nodes, strategy):
_match = strategy
if _match == 'innerHTML':
if sx_truthy(dom_is_fragment_p(new_nodes)):
return morph_children(target, new_nodes)
else:
wrapper = dom_create_element('div', NIL)
dom_append(wrapper, new_nodes)
return morph_children(target, wrapper)
elif _match == 'outerHTML':
parent = dom_parent(target)
((lambda fc: (_sx_begin(morph_node(target, fc), (lambda sib: insert_remaining_siblings(parent, target, sib))(dom_next_sibling(fc))) if sx_truthy(fc) else dom_remove_child(parent, target)))(dom_first_child(new_nodes)) if sx_truthy(dom_is_fragment_p(new_nodes)) else morph_node(target, new_nodes))
return parent
elif _match == 'afterend':
return dom_insert_after(target, new_nodes)
elif _match == 'beforeend':
return dom_append(target, new_nodes)
elif _match == 'afterbegin':
return dom_prepend(target, new_nodes)
elif _match == 'beforebegin':
return dom_insert_before(dom_parent(target), new_nodes, target)
elif _match == 'delete':
return dom_remove_child(dom_parent(target), target)
elif _match == 'none':
return NIL
else:
if sx_truthy(dom_is_fragment_p(new_nodes)):
return morph_children(target, new_nodes)
else:
wrapper = dom_create_element('div', NIL)
dom_append(wrapper, new_nodes)
return morph_children(target, wrapper)
# insert-remaining-siblings
def insert_remaining_siblings(parent, ref_node, sib):
if sx_truthy(sib):
next = dom_next_sibling(sib)
dom_insert_after(ref_node, sib)
return insert_remaining_siblings(parent, sib, next)
return NIL
# swap-html-string
def swap_html_string(target, html, strategy):
_match = strategy
if _match == 'innerHTML':
return dom_set_inner_html(target, html)
elif _match == 'outerHTML':
parent = dom_parent(target)
dom_insert_adjacent_html(target, 'afterend', html)
dom_remove_child(parent, target)
return parent
elif _match == 'afterend':
return dom_insert_adjacent_html(target, 'afterend', html)
elif _match == 'beforeend':
return dom_insert_adjacent_html(target, 'beforeend', html)
elif _match == 'afterbegin':
return dom_insert_adjacent_html(target, 'afterbegin', html)
elif _match == 'beforebegin':
return dom_insert_adjacent_html(target, 'beforebegin', html)
elif _match == 'delete':
return dom_remove_child(dom_parent(target), target)
elif _match == 'none':
return NIL
else:
return dom_set_inner_html(target, html)
# handle-history
def handle_history(el, url, resp_headers):
push_url = dom_get_attr(el, 'sx-push-url')
replace_url = dom_get_attr(el, 'sx-replace-url')
hdr_replace = get(resp_headers, 'replace-url')
if sx_truthy(hdr_replace):
return browser_replace_state(hdr_replace)
elif sx_truthy((push_url if not sx_truthy(push_url) else (not sx_truthy((push_url == 'false'))))):
return browser_push_state((url if sx_truthy((push_url == 'true')) else push_url))
elif sx_truthy((replace_url if not sx_truthy(replace_url) else (not sx_truthy((replace_url == 'false'))))):
return browser_replace_state((url if sx_truthy((replace_url == 'true')) else replace_url))
return NIL
# PRELOAD_TTL
PRELOAD_TTL = 30000
# preload-cache-get
def preload_cache_get(cache, url):
entry = dict_get(cache, url)
if sx_truthy(is_nil(entry)):
return NIL
else:
if sx_truthy(((now_ms() - get(entry, 'timestamp')) > PRELOAD_TTL)):
dict_delete(cache, url)
return NIL
else:
dict_delete(cache, url)
return entry
# preload-cache-set
def preload_cache_set(cache, url, text, content_type):
return _sx_dict_set(cache, url, {'text': text, 'content-type': content_type, 'timestamp': now_ms()})
# classify-trigger
def classify_trigger(trigger):
event = get(trigger, 'event')
if sx_truthy((event == 'every')):
return 'poll'
elif sx_truthy((event == 'intersect')):
return 'intersect'
elif sx_truthy((event == 'load')):
return 'load'
elif sx_truthy((event == 'revealed')):
return 'revealed'
else:
return 'event'
# should-boost-link?
def should_boost_link_p(link):
href = dom_get_attr(link, 'href')
return (href if not sx_truthy(href) else ((not sx_truthy(starts_with_p(href, '#'))) if not sx_truthy((not sx_truthy(starts_with_p(href, '#')))) else ((not sx_truthy(starts_with_p(href, 'javascript:'))) if not sx_truthy((not sx_truthy(starts_with_p(href, 'javascript:')))) else ((not sx_truthy(starts_with_p(href, 'mailto:'))) if not sx_truthy((not sx_truthy(starts_with_p(href, 'mailto:')))) else (browser_same_origin_p(href) if not sx_truthy(browser_same_origin_p(href)) else ((not sx_truthy(dom_has_attr_p(link, 'sx-get'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(link, 'sx-get')))) else ((not sx_truthy(dom_has_attr_p(link, 'sx-post'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(link, 'sx-post')))) else (not sx_truthy(dom_has_attr_p(link, 'sx-disable'))))))))))
# should-boost-form?
def should_boost_form_p(form):
return ((not sx_truthy(dom_has_attr_p(form, 'sx-get'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(form, 'sx-get')))) else ((not sx_truthy(dom_has_attr_p(form, 'sx-post'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(form, 'sx-post')))) else (not sx_truthy(dom_has_attr_p(form, 'sx-disable')))))
# parse-sse-swap
def parse_sse_swap(el):
return (dom_get_attr(el, 'sx-sse-swap') if sx_truthy(dom_get_attr(el, 'sx-sse-swap')) else 'message')
# === Transpiled from router (client-side route matching) ===
# split-path-segments
def split_path_segments(path):
trimmed = (slice(path, 1) if sx_truthy(starts_with_p(path, '/')) else path)
trimmed2 = (slice(trimmed, 0, (len(trimmed) - 1)) if sx_truthy(((not sx_truthy(empty_p(trimmed))) if not sx_truthy((not sx_truthy(empty_p(trimmed)))) else ends_with_p(trimmed, '/'))) else trimmed)
if sx_truthy(empty_p(trimmed2)):
return []
else:
return split(trimmed2, '/')
# make-route-segment
def make_route_segment(seg):
if sx_truthy((starts_with_p(seg, '<') if not sx_truthy(starts_with_p(seg, '<')) else ends_with_p(seg, '>'))):
param_name = slice(seg, 1, (len(seg) - 1))
d = {}
d['type'] = 'param'
d['value'] = param_name
return d
else:
d = {}
d['type'] = 'literal'
d['value'] = seg
return d
# parse-route-pattern
def parse_route_pattern(pattern):
segments = split_path_segments(pattern)
return map(make_route_segment, segments)
# match-route-segments
def match_route_segments(path_segs, parsed_segs):
_cells = {}
if sx_truthy((not sx_truthy((len(path_segs) == len(parsed_segs))))):
return NIL
else:
params = {}
_cells['matched'] = True
for_each_indexed(lambda i, parsed_seg: ((lambda path_seg: (lambda seg_type: ((_sx_cell_set(_cells, 'matched', False) if sx_truthy((not sx_truthy((path_seg == get(parsed_seg, 'value'))))) else NIL) if sx_truthy((seg_type == 'literal')) else (_sx_dict_set(params, get(parsed_seg, 'value'), path_seg) if sx_truthy((seg_type == 'param')) else _sx_cell_set(_cells, 'matched', False))))(get(parsed_seg, 'type')))(nth(path_segs, i)) if sx_truthy(_cells['matched']) else NIL), parsed_segs)
if sx_truthy(_cells['matched']):
return params
else:
return NIL
# match-route
def match_route(path, pattern):
path_segs = split_path_segments(path)
parsed_segs = parse_route_pattern(pattern)
return match_route_segments(path_segs, parsed_segs)
# find-matching-route
def find_matching_route(path, routes):
_cells = {}
path_segs = split_path_segments(path)
_cells['result'] = NIL
for route in routes:
if sx_truthy(is_nil(_cells['result'])):
params = match_route_segments(path_segs, get(route, 'parsed'))
if sx_truthy((not sx_truthy(is_nil(params)))):
matched = merge(route, {})
matched['params'] = params
_cells['result'] = matched
return _cells['result']
# === Transpiled from signals (reactive signal runtime) ===
# signal

285
shared/sx/ref/test-types.sx Normal file
View File

@@ -0,0 +1,285 @@
;; ==========================================================================
;; test-types.sx — Tests for the SX gradual type system
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: types.sx (subtype?, infer-type, check-component, etc.)
;;
;; Platform functions required (beyond test framework):
;; All type system functions from types.sx must be loaded.
;; test-prim-types — a dict of primitive return types for testing.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Subtype checking
;; --------------------------------------------------------------------------
(defsuite "subtype-basics"
(deftest "any accepts everything"
(assert-true (subtype? "number" "any"))
(assert-true (subtype? "string" "any"))
(assert-true (subtype? "nil" "any"))
(assert-true (subtype? "boolean" "any"))
(assert-true (subtype? "any" "any")))
(deftest "never is subtype of everything"
(assert-true (subtype? "never" "number"))
(assert-true (subtype? "never" "string"))
(assert-true (subtype? "never" "any"))
(assert-true (subtype? "never" "nil")))
(deftest "identical types"
(assert-true (subtype? "number" "number"))
(assert-true (subtype? "string" "string"))
(assert-true (subtype? "boolean" "boolean"))
(assert-true (subtype? "nil" "nil")))
(deftest "different base types are not subtypes"
(assert-false (subtype? "number" "string"))
(assert-false (subtype? "string" "number"))
(assert-false (subtype? "boolean" "number"))
(assert-false (subtype? "string" "boolean")))
(deftest "any is not subtype of specific type"
(assert-false (subtype? "any" "number"))
(assert-false (subtype? "any" "string"))))
(defsuite "subtype-nullable"
(deftest "nil is subtype of nullable types"
(assert-true (subtype? "nil" "string?"))
(assert-true (subtype? "nil" "number?"))
(assert-true (subtype? "nil" "dict?"))
(assert-true (subtype? "nil" "boolean?")))
(deftest "base is subtype of its nullable"
(assert-true (subtype? "string" "string?"))
(assert-true (subtype? "number" "number?"))
(assert-true (subtype? "dict" "dict?")))
(deftest "nullable is not subtype of base"
(assert-false (subtype? "string?" "string"))
(assert-false (subtype? "number?" "number")))
(deftest "different nullable types are not subtypes"
(assert-false (subtype? "number" "string?"))
(assert-false (subtype? "string" "number?"))))
(defsuite "subtype-unions"
(deftest "member is subtype of union"
(assert-true (subtype? "number" (list "or" "number" "string")))
(assert-true (subtype? "string" (list "or" "number" "string"))))
(deftest "non-member is not subtype of union"
(assert-false (subtype? "boolean" (list "or" "number" "string"))))
(deftest "union is subtype if all members are"
(assert-true (subtype? (list "or" "number" "string")
(list "or" "number" "string" "boolean")))
(assert-true (subtype? (list "or" "number" "string") "any")))
(deftest "union is not subtype if any member is not"
(assert-false (subtype? (list "or" "number" "string") "number"))))
(defsuite "subtype-list-of"
(deftest "list-of covariance"
(assert-true (subtype? (list "list-of" "number") (list "list-of" "number")))
(assert-true (subtype? (list "list-of" "number") (list "list-of" "any"))))
(deftest "list-of is subtype of list"
(assert-true (subtype? (list "list-of" "number") "list")))
(deftest "list is subtype of list-of any"
(assert-true (subtype? "list" (list "list-of" "any")))))
;; --------------------------------------------------------------------------
;; Type union
;; --------------------------------------------------------------------------
(defsuite "type-union"
(deftest "same types"
(assert-equal "number" (type-union "number" "number"))
(assert-equal "string" (type-union "string" "string")))
(deftest "any absorbs"
(assert-equal "any" (type-union "any" "number"))
(assert-equal "any" (type-union "number" "any")))
(deftest "never is identity"
(assert-equal "number" (type-union "never" "number"))
(assert-equal "string" (type-union "string" "never")))
(deftest "nil + base creates nullable"
(assert-equal "string?" (type-union "nil" "string"))
(assert-equal "number?" (type-union "number" "nil")))
(deftest "subtype collapses"
(assert-equal "string?" (type-union "string" "string?"))
(assert-equal "string?" (type-union "string?" "string")))
(deftest "incompatible creates union"
(let ((result (type-union "number" "string")))
(assert-true (= (type-of result) "list"))
(assert-equal "or" (first result))
(assert-true (contains? result "number"))
(assert-true (contains? result "string")))))
;; --------------------------------------------------------------------------
;; Type narrowing
;; --------------------------------------------------------------------------
(defsuite "type-narrowing"
(deftest "nil? narrows to nil in then branch"
(let ((result (narrow-type "string?" "nil?")))
(assert-equal "nil" (first result))
(assert-equal "string" (nth result 1))))
(deftest "nil? narrows any stays any"
(let ((result (narrow-type "any" "nil?")))
(assert-equal "nil" (first result))
(assert-equal "any" (nth result 1))))
(deftest "string? narrows to string in then branch"
(let ((result (narrow-type "any" "string?")))
(assert-equal "string" (first result))
;; else branch — can't narrow any
(assert-equal "any" (nth result 1))))
(deftest "nil? on nil type narrows to never in else"
(let ((result (narrow-type "nil" "nil?")))
(assert-equal "nil" (first result))
(assert-equal "never" (nth result 1)))))
;; --------------------------------------------------------------------------
;; Type inference
;; --------------------------------------------------------------------------
(defsuite "infer-literals"
(deftest "number literal"
(assert-equal "number" (infer-type 42 (dict) (test-prim-types))))
(deftest "string literal"
(assert-equal "string" (infer-type "hello" (dict) (test-prim-types))))
(deftest "boolean literal"
(assert-equal "boolean" (infer-type true (dict) (test-prim-types))))
(deftest "nil"
(assert-equal "nil" (infer-type nil (dict) (test-prim-types)))))
(defsuite "infer-calls"
(deftest "known primitive return type"
;; (+ 1 2) → number
(let ((expr (sx-parse "(+ 1 2)")))
(assert-equal "number"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "str returns string"
(let ((expr (sx-parse "(str 1 2)")))
(assert-equal "string"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "comparison returns boolean"
(let ((expr (sx-parse "(= 1 2)")))
(assert-equal "boolean"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "component call returns element"
(let ((expr (sx-parse "(~card :title \"hi\")")))
(assert-equal "element"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "unknown function returns any"
(let ((expr (sx-parse "(unknown-fn 1 2)")))
(assert-equal "any"
(infer-type (first expr) (dict) (test-prim-types))))))
(defsuite "infer-special-forms"
(deftest "if produces union of branches"
(let ((expr (sx-parse "(if true 42 \"hello\")")))
(let ((t (infer-type (first expr) (dict) (test-prim-types))))
;; number | string — should be a union
(assert-true (or (= t (list "or" "number" "string"))
(= t "any"))))))
(deftest "if with no else includes nil"
(let ((expr (sx-parse "(if true 42)")))
(let ((t (infer-type (first expr) (dict) (test-prim-types))))
(assert-equal "number?" t))))
(deftest "when includes nil"
(let ((expr (sx-parse "(when true 42)")))
(let ((t (infer-type (first expr) (dict) (test-prim-types))))
(assert-equal "number?" t))))
(deftest "do returns last type"
(let ((expr (sx-parse "(do 1 2 \"hello\")")))
(assert-equal "string"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "let infers binding types"
(let ((expr (sx-parse "(let ((x 42)) x)")))
(assert-equal "number"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "lambda returns lambda"
(let ((expr (sx-parse "(fn (x) (+ x 1))")))
(assert-equal "lambda"
(infer-type (first expr) (dict) (test-prim-types))))))
;; --------------------------------------------------------------------------
;; Component call checking
;; --------------------------------------------------------------------------
(defsuite "check-component-calls"
(deftest "type mismatch produces error"
;; Create a component with typed params, then check a bad call
(let ((env (test-env)))
;; Define a typed component
(do
(define dummy-env env)
(defcomp ~typed-card (&key title price) (div title price))
(component-set-param-types! ~typed-card
{:title "string" :price "number"}))
;; Check a call with wrong type
(let ((diagnostics
(check-component-call "~typed-card" ~typed-card
(rest (first (sx-parse "(~typed-card :title 42 :price \"bad\")")))
(dict) (test-prim-types))))
(assert-true (> (len diagnostics) 0))
(assert-equal "error" (dict-get (first diagnostics) "level")))))
(deftest "correct call produces no errors"
(let ((env (test-env)))
(do
(define dummy-env env)
(defcomp ~ok-card (&key title price) (div title price))
(component-set-param-types! ~ok-card
{:title "string" :price "number"}))
(let ((diagnostics
(check-component-call "~ok-card" ~ok-card
(rest (first (sx-parse "(~ok-card :title \"hi\" :price 42)")))
(dict) (test-prim-types))))
(assert-equal 0 (len diagnostics)))))
(deftest "unknown kwarg produces warning"
(let ((env (test-env)))
(do
(define dummy-env env)
(defcomp ~warn-card (&key title) (div title))
(component-set-param-types! ~warn-card
{:title "string"}))
(let ((diagnostics
(check-component-call "~warn-card" ~warn-card
(rest (first (sx-parse "(~warn-card :title \"hi\" :colour \"red\")")))
(dict) (test-prim-types))))
(assert-true (> (len diagnostics) 0))
(assert-equal "warning" (dict-get (first diagnostics) "level"))))))

601
shared/sx/ref/types.sx Normal file
View File

@@ -0,0 +1,601 @@
;; ==========================================================================
;; types.sx — Gradual type system for SX
;;
;; Registration-time type checking: zero runtime cost.
;; Annotations are optional — unannotated code defaults to `any`.
;;
;; Depends on: eval.sx (type-of, component accessors, env ops)
;; primitives.sx, boundary.sx (return type declarations)
;;
;; Platform interface (from eval.sx, already provided):
;; (type-of x) → type string
;; (symbol-name s) → string
;; (keyword-name k) → string
;; (component-body c) → AST
;; (component-params c) → list of param name strings
;; (component-has-children c) → boolean
;; (env-get env k) → value or nil
;;
;; New platform functions for types.sx:
;; (component-param-types c) → dict {param-name → type} or nil
;; (component-set-param-types! c d) → store param types on component
;; ==========================================================================
;; --------------------------------------------------------------------------
;; 1. Type representation
;; --------------------------------------------------------------------------
;; Types are plain SX values:
;; - Strings for base types: "number", "string", "boolean", "nil",
;; "symbol", "keyword", "element", "any", "never"
;; - Nullable shorthand: "string?", "number?", "dict?", "boolean?"
;; → equivalent to (or string nil) etc.
;; - Lists for compound types:
;; (or t1 t2 ...) — union
;; (list-of t) — homogeneous list
;; (dict-of tk tv) — typed dict
;; (-> t1 t2 ... treturn) — function type (last is return)
;; Base type names
(define base-types
(list "number" "string" "boolean" "nil" "symbol" "keyword"
"element" "any" "never" "list" "dict"
"lambda" "component" "island" "macro" "signal"))
;; --------------------------------------------------------------------------
;; 2. Type predicates
;; --------------------------------------------------------------------------
(define type-any?
(fn (t) (= t "any")))
(define type-never?
(fn (t) (= t "never")))
(define type-nullable?
(fn (t)
;; A type is nullable if it's "any", "nil", a "?" shorthand, or
;; a union containing "nil".
(if (= t "any") true
(if (= t "nil") true
(if (and (= (type-of t) "string") (ends-with? t "?")) true
(if (and (= (type-of t) "list")
(not (empty? t))
(= (first t) "or"))
(contains? (rest t) "nil")
false))))))
(define nullable-base
(fn (t)
;; Strip "?" from nullable shorthand: "string?" → "string"
(if (and (= (type-of t) "string")
(ends-with? t "?")
(not (= t "?")))
(slice t 0 (- (string-length t) 1))
t)))
;; --------------------------------------------------------------------------
;; 3. Subtype checking
;; --------------------------------------------------------------------------
;; subtype?(a, b) — is type `a` assignable to type `b`?
(define subtype?
(fn (a b)
;; any accepts everything
(if (type-any? b) true
;; never is subtype of everything
(if (type-never? a) true
;; any is not a subtype of a specific type
(if (type-any? a) false
;; identical types
(if (= a b) true
;; nil is subtype of nullable types
(if (= a "nil")
(type-nullable? b)
;; nullable shorthand: "string?" = (or string nil)
(if (and (= (type-of b) "string") (ends-with? b "?"))
(let ((base (nullable-base b)))
(or (= a base) (= a "nil")))
;; a is a union: (or t1 t2 ...) <: b if ALL members <: b
;; Must check before b-union — (or A B) <: (or A B C) needs
;; each member of a checked against the full union b.
(if (and (= (type-of a) "list")
(not (empty? a))
(= (first a) "or"))
(every? (fn (member) (subtype? member b)) (rest a))
;; union: a <: (or t1 t2 ...) if a <: any member
(if (and (= (type-of b) "list")
(not (empty? b))
(= (first b) "or"))
(some (fn (member) (subtype? a member)) (rest b))
;; list-of covariance
(if (and (= (type-of a) "list") (= (type-of b) "list")
(= (len a) 2) (= (len b) 2)
(= (first a) "list-of") (= (first b) "list-of"))
(subtype? (nth a 1) (nth b 1))
;; "list" <: (list-of any)
(if (and (= a "list")
(= (type-of b) "list")
(= (len b) 2)
(= (first b) "list-of"))
(type-any? (nth b 1))
;; (list-of t) <: "list"
(if (and (= (type-of a) "list")
(= (len a) 2)
(= (first a) "list-of")
(= b "list"))
true
;; "element" is subtype of "string?" (rendered HTML)
false)))))))))))))
;; --------------------------------------------------------------------------
;; 4. Type union
;; --------------------------------------------------------------------------
(define type-union
(fn (a b)
;; Compute the smallest type that encompasses both a and b.
(if (= a b) a
(if (type-any? a) "any"
(if (type-any? b) "any"
(if (type-never? a) b
(if (type-never? b) a
(if (subtype? a b) b
(if (subtype? b a) a
;; neither is subtype — create a union
(if (= a "nil")
;; nil + string → string?
(if (and (= (type-of b) "string")
(not (ends-with? b "?")))
(str b "?")
(list "or" a b))
(if (= b "nil")
(if (and (= (type-of a) "string")
(not (ends-with? a "?")))
(str a "?")
(list "or" a b))
(list "or" a b))))))))))))
;; --------------------------------------------------------------------------
;; 5. Type narrowing
;; --------------------------------------------------------------------------
(define narrow-type
(fn (t predicate-name)
;; Narrow type based on a predicate test in a truthy branch.
;; (if (nil? x) ..then.. ..else..) → in else, x excludes nil.
;; Returns (narrowed-then narrowed-else).
(if (= predicate-name "nil?")
(list "nil" (narrow-exclude-nil t))
(if (= predicate-name "string?")
(list "string" (narrow-exclude t "string"))
(if (= predicate-name "number?")
(list "number" (narrow-exclude t "number"))
(if (= predicate-name "list?")
(list "list" (narrow-exclude t "list"))
(if (= predicate-name "dict?")
(list "dict" (narrow-exclude t "dict"))
(if (= predicate-name "boolean?")
(list "boolean" (narrow-exclude t "boolean"))
;; Unknown predicate — no narrowing
(list t t)))))))))
(define narrow-exclude-nil
(fn (t)
;; Remove nil from a type.
(if (= t "nil") "never"
(if (= t "any") "any" ;; can't narrow any
(if (and (= (type-of t) "string") (ends-with? t "?"))
(nullable-base t)
(if (and (= (type-of t) "list")
(not (empty? t))
(= (first t) "or"))
(let ((members (filter (fn (m) (not (= m "nil"))) (rest t))))
(if (= (len members) 1) (first members)
(if (empty? members) "never"
(cons "or" members))))
t))))))
(define narrow-exclude
(fn (t excluded)
;; Remove a specific type from a union.
(if (= t excluded) "never"
(if (= t "any") "any"
(if (and (= (type-of t) "list")
(not (empty? t))
(= (first t) "or"))
(let ((members (filter (fn (m) (not (= m excluded))) (rest t))))
(if (= (len members) 1) (first members)
(if (empty? members) "never"
(cons "or" members))))
t)))))
;; --------------------------------------------------------------------------
;; 6. Type inference
;; --------------------------------------------------------------------------
;; infer-type walks an AST node and returns its inferred type.
;; type-env is a dict mapping variable names → types.
(define infer-type
(fn (node type-env prim-types)
(let ((kind (type-of node)))
(if (= kind "number") "number"
(if (= kind "string") "string"
(if (= kind "boolean") "boolean"
(if (nil? node) "nil"
(if (= kind "keyword") "keyword"
(if (= kind "symbol")
(let ((name (symbol-name node)))
;; Look up in type env
(if (dict-has? type-env name)
(dict-get type-env name)
;; Builtins
(if (= name "true") "boolean"
(if (= name "false") "boolean"
(if (= name "nil") "nil"
;; Check primitive return types
(if (dict-has? prim-types name)
(dict-get prim-types name)
"any"))))))
(if (= kind "dict") "dict"
(if (= kind "list")
(infer-list-type node type-env prim-types)
"any")))))))))))
(define infer-list-type
(fn (node type-env prim-types)
;; Infer type of a list expression (function call, special form, etc.)
(if (empty? node) "list"
(let ((head (first node))
(args (rest node)))
(if (not (= (type-of head) "symbol"))
"any" ;; complex head — can't infer
(let ((name (symbol-name head)))
;; Special forms
(if (= name "if")
(infer-if-type args type-env prim-types)
(if (= name "when")
(if (>= (len args) 2)
(type-union (infer-type (last args) type-env prim-types) "nil")
"nil")
(if (or (= name "cond") (= name "case"))
"any" ;; complex — could be refined later
(if (= name "let")
(infer-let-type args type-env prim-types)
(if (or (= name "do") (= name "begin"))
(if (empty? args) "nil"
(infer-type (last args) type-env prim-types))
(if (or (= name "lambda") (= name "fn"))
"lambda"
(if (= name "and")
(if (empty? args) "boolean"
(infer-type (last args) type-env prim-types))
(if (= name "or")
(if (empty? args) "boolean"
;; or returns first truthy — union of all args
(reduce type-union "never"
(map (fn (a) (infer-type a type-env prim-types)) args)))
(if (= name "map")
;; map returns a list
(if (>= (len args) 2)
(let ((fn-type (infer-type (first args) type-env prim-types)))
;; If the fn's return type is known, produce (list-of return-type)
(if (and (= (type-of fn-type) "list")
(= (first fn-type) "->"))
(list "list-of" (last fn-type))
"list"))
"list")
(if (= name "filter")
;; filter preserves element type
(if (>= (len args) 2)
(infer-type (nth args 1) type-env prim-types)
"list")
(if (= name "reduce")
;; reduce returns the accumulator type — too complex to infer
"any"
(if (= name "list")
"list"
(if (= name "dict")
"dict"
(if (= name "quote")
"any"
(if (= name "str")
"string"
(if (= name "not")
"boolean"
(if (starts-with? name "~")
"element" ;; component call
;; Regular function call: look up return type
(if (dict-has? prim-types name)
(dict-get prim-types name)
"any"))))))))))))))))))))))))
(define infer-if-type
(fn (args type-env prim-types)
;; (if test then else?) → union of then and else types
(if (< (len args) 2) "nil"
(let ((then-type (infer-type (nth args 1) type-env prim-types)))
(if (>= (len args) 3)
(type-union then-type (infer-type (nth args 2) type-env prim-types))
(type-union then-type "nil"))))))
(define infer-let-type
(fn (args type-env prim-types)
;; (let ((x expr) ...) body) → type of body in extended type-env
(if (< (len args) 2) "nil"
(let ((bindings (first args))
(body (last args))
(extended (merge type-env (dict))))
;; Add binding types
(for-each
(fn (binding)
(when (and (= (type-of binding) "list") (>= (len binding) 2))
(let ((name (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(str (first binding))))
(val-type (infer-type (nth binding 1) extended prim-types)))
(dict-set! extended name val-type))))
bindings)
(infer-type body extended prim-types)))))
;; --------------------------------------------------------------------------
;; 7. Diagnostic types
;; --------------------------------------------------------------------------
;; Diagnostics are dicts:
;; {:level "error"|"warning"|"info"
;; :message "human-readable explanation"
;; :component "~name" (or nil for top-level)
;; :expr <the offending AST node>}
(define make-diagnostic
(fn (level message component expr)
{:level level
:message message
:component component
:expr expr}))
;; --------------------------------------------------------------------------
;; 8. Call-site checking
;; --------------------------------------------------------------------------
(define check-primitive-call
(fn (name args type-env prim-types)
;; Check a primitive call site. Returns list of diagnostics.
(let ((diagnostics (list)))
;; Currently just checks return types are used correctly.
;; Phase 5 adds param type checking when primitives.sx has
;; typed params.
diagnostics)))
(define check-component-call
(fn (comp-name comp call-args type-env prim-types)
;; Check a component call site against its declared param types.
;; comp is the component value, call-args is the list of args
;; from the call site (after the component name).
(let ((diagnostics (list))
(param-types (component-param-types comp))
(params (component-params comp)))
(when (and (not (nil? param-types))
(not (empty? (keys param-types))))
;; Parse keyword args from call site
(let ((i 0)
(provided-keys (list)))
(for-each
(fn (idx)
(when (< idx (len call-args))
(let ((arg (nth call-args idx)))
(when (= (type-of arg) "keyword")
(let ((key-name (keyword-name arg)))
(append! provided-keys key-name)
(when (< (+ idx 1) (len call-args))
(let ((val-expr (nth call-args (+ idx 1))))
;; Check type of value against declared param type
(when (dict-has? param-types key-name)
(let ((expected (dict-get param-types key-name))
(actual (infer-type val-expr type-env prim-types)))
(when (and (not (type-any? expected))
(not (type-any? actual))
(not (subtype? actual expected)))
(append! diagnostics
(make-diagnostic "error"
(str "Keyword :" key-name " of " comp-name
" expects " expected ", got " actual)
comp-name val-expr))))))))))))
(range 0 (len call-args) 1))
;; Check for missing required params (those with declared types)
(for-each
(fn (param-name)
(when (and (dict-has? param-types param-name)
(not (contains? provided-keys param-name))
(not (type-nullable? (dict-get param-types param-name))))
(append! diagnostics
(make-diagnostic "warning"
(str "Required param :" param-name " of " comp-name " not provided")
comp-name nil))))
params)
;; Check for unknown kwargs
(for-each
(fn (key)
(when (not (contains? params key))
(append! diagnostics
(make-diagnostic "warning"
(str "Unknown keyword :" key " passed to " comp-name)
comp-name nil))))
provided-keys)))
diagnostics)))
;; --------------------------------------------------------------------------
;; 9. AST walker — check a component body
;; --------------------------------------------------------------------------
(define check-body-walk
(fn (node comp-name type-env prim-types env diagnostics)
;; Recursively walk an AST and collect diagnostics.
(let ((kind (type-of node)))
(when (= kind "list")
(when (not (empty? node))
(let ((head (first node))
(args (rest node)))
;; Check component calls
(when (= (type-of head) "symbol")
(let ((name (symbol-name head)))
;; Component call
(when (starts-with? name "~")
(let ((comp-val (env-get env name)))
(when (= (type-of comp-val) "component")
(for-each
(fn (d) (append! diagnostics d))
(check-component-call name comp-val args
type-env prim-types)))))
;; Recurse into let with extended type env
(when (or (= name "let") (= name "let*"))
(when (>= (len args) 2)
(let ((bindings (first args))
(body-exprs (rest args))
(extended (merge type-env (dict))))
(for-each
(fn (binding)
(when (and (= (type-of binding) "list")
(>= (len binding) 2))
(let ((bname (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(str (first binding))))
(val-type (infer-type (nth binding 1) extended prim-types)))
(dict-set! extended bname val-type))))
bindings)
(for-each
(fn (body)
(check-body-walk body comp-name extended prim-types env diagnostics))
body-exprs))))
;; Recurse into define with type binding
(when (= name "define")
(when (>= (len args) 2)
(let ((def-name (if (= (type-of (first args)) "symbol")
(symbol-name (first args))
nil))
(def-val (nth args 1)))
(when def-name
(dict-set! type-env def-name
(infer-type def-val type-env prim-types)))
(check-body-walk def-val comp-name type-env prim-types env diagnostics))))))
;; Recurse into all child expressions
(for-each
(fn (child)
(check-body-walk child comp-name type-env prim-types env diagnostics))
args)))))))
;; --------------------------------------------------------------------------
;; 10. Check a single component
;; --------------------------------------------------------------------------
(define check-component
(fn (comp-name env prim-types)
;; Type-check a component's body. Returns list of diagnostics.
(let ((comp (env-get env comp-name))
(diagnostics (list)))
(when (= (type-of comp) "component")
(let ((body (component-body comp))
(params (component-params comp))
(param-types (component-param-types comp))
;; Build initial type env from component params
(type-env (dict)))
;; Add param types (annotated or default to any)
(for-each
(fn (p)
(dict-set! type-env p
(if (and (not (nil? param-types))
(dict-has? param-types p))
(dict-get param-types p)
"any")))
params)
;; Add children as (list-of element) if component has children
(when (component-has-children comp)
(dict-set! type-env "children" (list "list-of" "element")))
(check-body-walk body comp-name type-env prim-types env diagnostics)))
diagnostics)))
;; --------------------------------------------------------------------------
;; 11. Check all components in an environment
;; --------------------------------------------------------------------------
(define check-all
(fn (env prim-types)
;; Type-check every component in the environment.
;; Returns list of all diagnostics.
(let ((all-diagnostics (list)))
(for-each
(fn (name)
(let ((val (env-get env name)))
(when (= (type-of val) "component")
(for-each
(fn (d) (append! all-diagnostics d))
(check-component name env prim-types)))))
(keys env))
all-diagnostics)))
;; --------------------------------------------------------------------------
;; 12. Build primitive type registry
;; --------------------------------------------------------------------------
;; Builds a dict mapping primitive-name → return-type from
;; the declarations parsed by boundary_parser.py.
;; This is called by the host at startup with the parsed declarations.
(define build-type-registry
(fn (prim-declarations io-declarations)
;; Both are lists of dicts: {:name "+" :returns "number" :params (...)}
;; Returns a flat dict: {"+" "number", "str" "string", ...}
(let ((registry (dict)))
(for-each
(fn (decl)
(let ((name (dict-get decl "name"))
(returns (dict-get decl "returns")))
(when (and (not (nil? name)) (not (nil? returns)))
(dict-set! registry name returns))))
prim-declarations)
(for-each
(fn (decl)
(let ((name (dict-get decl "name"))
(returns (dict-get decl "returns")))
(when (and (not (nil? name)) (not (nil? returns)))
(dict-set! registry name returns))))
io-declarations)
registry)))
;; --------------------------------------------------------------------------
;; Platform interface summary
;; --------------------------------------------------------------------------
;;
;; From eval.sx (already provided):
;; (type-of x), (symbol-name s), (keyword-name k), (env-get env k)
;; (component-body c), (component-params c), (component-has-children c)
;;
;; New for types.sx (each host implements):
;; (component-param-types c) → dict {param-name → type} or nil
;; (component-set-param-types! c d) → store param types on component
;; (merge d1 d2) → new dict merging d1 and d2
;;
;; --------------------------------------------------------------------------

View File

@@ -262,6 +262,7 @@ SPECS = {
"engine": {"file": "test-engine.sx", "needs": []},
"orchestration": {"file": "test-orchestration.sx", "needs": []},
"signals": {"file": "test-signals.sx", "needs": ["make-signal"]},
"types": {"file": "test-types.sx", "needs": []},
}
REF_DIR = os.path.join(_HERE, "..", "ref")
@@ -722,6 +723,78 @@ def _load_signals(env):
env["batch"] = _batch
def _load_types(env):
"""Load types.sx spec — gradual type system."""
from shared.sx.types import Component
def _component_param_types(c):
return getattr(c, 'param_types', None)
def _component_set_param_types(c, d):
c.param_types = d
env["component-param-types"] = _component_param_types
env["component-set-param-types!"] = _component_set_param_types
# test-prim-types: a minimal type registry for testing
def _test_prim_types():
return {
"+": "number", "-": "number", "*": "number", "/": "number",
"mod": "number", "abs": "number", "floor": "number",
"ceil": "number", "round": "number", "min": "number",
"max": "number", "parse-int": "number", "parse-float": "number",
"=": "boolean", "!=": "boolean", "<": "boolean", ">": "boolean",
"<=": "boolean", ">=": "boolean",
"str": "string", "string-length": "number",
"substring": "string", "upcase": "string", "downcase": "string",
"trim": "string", "split": "list", "join": "string",
"string-contains?": "boolean", "starts-with?": "boolean",
"ends-with?": "boolean", "replace": "string",
"not": "boolean", "nil?": "boolean", "number?": "boolean",
"string?": "boolean", "list?": "boolean", "dict?": "boolean",
"boolean?": "boolean", "symbol?": "boolean", "empty?": "boolean",
"list": "list", "first": "any", "rest": "list", "nth": "any",
"last": "any", "cons": "list", "append": "list",
"reverse": "list", "len": "number", "contains?": "boolean",
"flatten": "list", "concat": "list", "slice": "list",
"range": "list", "sort": "list", "sort-by": "list",
"map": "list", "filter": "list", "reduce": "any",
"some": "boolean", "every?": "boolean",
"dict": "dict", "assoc": "dict", "dissoc": "dict",
"get": "any", "keys": "list", "vals": "list",
"has-key?": "boolean", "merge": "dict",
}
env["test-prim-types"] = _test_prim_types
env["test-env"] = lambda: env
# Try bootstrapped types first, fall back to eval
try:
from shared.sx.ref.sx_ref import (
subtype_p, type_union, narrow_type,
infer_type, check_component_call, check_component,
check_all, build_type_registry, type_any_p,
type_never_p, type_nullable_p, nullable_base,
narrow_exclude_nil, narrow_exclude,
)
env["subtype?"] = subtype_p
env["type-union"] = type_union
env["narrow-type"] = narrow_type
env["infer-type"] = infer_type
env["check-component-call"] = check_component_call
env["check-component"] = check_component
env["check-all"] = check_all
env["build-type-registry"] = build_type_registry
env["type-any?"] = type_any_p
env["type-never?"] = type_never_p
env["type-nullable?"] = type_nullable_p
env["nullable-base"] = nullable_base
env["narrow-exclude-nil"] = narrow_exclude_nil
env["narrow-exclude"] = narrow_exclude
except ImportError:
eval_file("types.sx", env)
def main():
global passed, failed, test_num
@@ -769,6 +842,8 @@ def main():
_load_orchestration(env)
if spec_name == "signals":
_load_signals(env)
if spec_name == "types":
_load_types(env)
print(f"# --- {spec_name} ---")
eval_file(spec["file"], env)

View File

@@ -170,6 +170,7 @@ class Component:
deps: set[str] = field(default_factory=set) # transitive component deps (~names)
io_refs: set[str] | None = None # transitive IO primitive refs (None = not computed)
affinity: str = "auto" # "auto" | "client" | "server"
param_types: dict[str, Any] | None = None # {param_name: type_expr} for gradual typing
@property
def is_pure(self) -> bool: