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:
@@ -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"}
|
||||
|
||||
@@ -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
285
shared/sx/ref/test-types.sx
Normal 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
601
shared/sx/ref/types.sx
Normal 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
|
||||
;;
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user