Add deftype and defeffect to SX type system (Phases 6-7)

Phase 6 — deftype: named type aliases, unions, records, and parameterized
types. Type definitions stored as plain dicts in *type-registry*. Includes
resolve-type for named type resolution, substitute-type-vars for
parameterized instantiation, subtype-resolved? for structural record
subtyping, and infer-type extension for record field type inference via get.

Phase 7 — defeffect: static effect annotations. Effects stored in
*effect-registry* and *effect-annotations*. Supports :effects keyword on
defcomp and define. Gradual: unannotated = all effects, empty list = pure.
check-body-walk validates effect containment at call sites.

Standard types defined: (maybe a), type-def, diagnostic, prim-param-sig.
Standard effects declared: io, mutation, render.

84/84 type system tests pass. Both Python and JS bootstrappers succeed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 22:51:19 +00:00
parent b8018ba385
commit ce7ad125b6
12 changed files with 938 additions and 80 deletions

View File

@@ -534,6 +534,11 @@ def strip_prefix(s, prefix):
return s[len(prefix):] if s.startswith(prefix) else s
def debug_log(*args):
import sys
print(*args, file=sys.stderr)
def error(msg):
raise EvalError(msg)
@@ -1255,6 +1260,10 @@ def eval_list(expr, env):
return sf_defquery(args, env)
elif sx_truthy((name == 'defaction')):
return sf_defaction(args, env)
elif sx_truthy((name == 'deftype')):
return sf_deftype(args, env)
elif sx_truthy((name == 'defeffect')):
return sf_defeffect(args, env)
elif sx_truthy((name == 'begin')):
return sf_begin(args, env)
elif sx_truthy((name == 'do')):
@@ -1504,10 +1513,18 @@ def sf_lambda(args, env):
# sf-define
def sf_define(args, env):
name_sym = first(args)
value = trampoline(eval_expr(nth(args, 1), env))
has_effects = ((len(args) >= 4) if not sx_truthy((len(args) >= 4)) else ((type_of(nth(args, 1)) == 'keyword') if not sx_truthy((type_of(nth(args, 1)) == 'keyword')) else (keyword_name(nth(args, 1)) == 'effects')))
val_idx = (3 if sx_truthy(((len(args) >= 4) if not sx_truthy((len(args) >= 4)) else ((type_of(nth(args, 1)) == 'keyword') if not sx_truthy((type_of(nth(args, 1)) == 'keyword')) else (keyword_name(nth(args, 1)) == 'effects')))) else 1)
value = trampoline(eval_expr(nth(args, val_idx), env))
if sx_truthy((is_lambda(value) if not sx_truthy(is_lambda(value)) else is_nil(lambda_name(value)))):
value.name = symbol_name(name_sym)
env[symbol_name(name_sym)] = value
if sx_truthy(has_effects):
effects_raw = nth(args, 2)
effect_list = (map(lambda e: (symbol_name(e) if sx_truthy((type_of(e) == 'symbol')) else sx_str(e)), effects_raw) if sx_truthy((type_of(effects_raw) == 'list')) else [sx_str(effects_raw)])
effect_anns = (env_get(env, '*effect-annotations*') if sx_truthy(env_has(env, '*effect-annotations*')) else {})
effect_anns[symbol_name(name_sym)] = effect_list
env['*effect-annotations*'] = effect_anns
return value
# sf-defcomp
@@ -1522,8 +1539,14 @@ def sf_defcomp(args, env):
param_types = nth(parsed, 2)
affinity = defcomp_kwarg(args, 'affinity', 'auto')
comp = make_component(comp_name, params, has_children, body, env, affinity)
effects = defcomp_kwarg(args, 'effects', NIL)
if sx_truthy(((not sx_truthy(is_nil(param_types))) if not sx_truthy((not sx_truthy(is_nil(param_types)))) else (not sx_truthy(empty_p(keys(param_types)))))):
component_set_param_types(comp, param_types)
if sx_truthy((not sx_truthy(is_nil(effects)))):
effect_list = (map(lambda e: (symbol_name(e) if sx_truthy((type_of(e) == 'symbol')) else sx_str(e)), effects) if sx_truthy((type_of(effects) == 'list')) else [sx_str(effects)])
effect_anns = (env_get(env, '*effect-annotations*') if sx_truthy(env_has(env, '*effect-annotations*')) else {})
effect_anns[symbol_name(name_sym)] = effect_list
env['*effect-annotations*'] = effect_anns
env[symbol_name(name_sym)] = comp
return comp
@@ -1610,6 +1633,62 @@ def sf_defstyle(args, env):
env[symbol_name(name_sym)] = value
return value
# make-type-def
def make_type_def(name, params, body):
return {'name': name, 'params': params, 'body': body}
# normalize-type-body
def normalize_type_body(body):
if sx_truthy(is_nil(body)):
return 'nil'
elif sx_truthy((type_of(body) == 'symbol')):
return symbol_name(body)
elif sx_truthy((type_of(body) == 'string')):
return body
elif sx_truthy((type_of(body) == 'keyword')):
return keyword_name(body)
elif sx_truthy((type_of(body) == 'dict')):
return map_dict(lambda k, v: normalize_type_body(v), body)
elif sx_truthy((type_of(body) == 'list')):
if sx_truthy(empty_p(body)):
return 'any'
else:
head = first(body)
head_name = (symbol_name(head) if sx_truthy((type_of(head) == 'symbol')) else sx_str(head))
if sx_truthy((head_name == 'union')):
return cons('or', map(normalize_type_body, rest(body)))
else:
return cons(head_name, map(normalize_type_body, rest(body)))
else:
return sx_str(body)
# sf-deftype
def sf_deftype(args, env):
name_or_form = first(args)
body_expr = nth(args, 1)
type_name = NIL
type_params = []
if sx_truthy((type_of(name_or_form) == 'symbol')):
type_name = symbol_name(name_or_form)
else:
if sx_truthy((type_of(name_or_form) == 'list')):
type_name = symbol_name(first(name_or_form))
type_params = map(lambda p: (symbol_name(p) if sx_truthy((type_of(p) == 'symbol')) else sx_str(p)), rest(name_or_form))
body = normalize_type_body(body_expr)
registry = (env_get(env, '*type-registry*') if sx_truthy(env_has(env, '*type-registry*')) else {})
registry[type_name] = make_type_def(type_name, type_params, body)
env['*type-registry*'] = registry
return NIL
# sf-defeffect
def sf_defeffect(args, env):
effect_name = (symbol_name(first(args)) if sx_truthy((type_of(first(args)) == 'symbol')) else sx_str(first(args)))
registry = (env_get(env, '*effect-registry*') if sx_truthy(env_has(env, '*effect-registry*')) else [])
if sx_truthy((not sx_truthy(contains_p(registry, effect_name)))):
registry.append(effect_name)
env['*effect-registry*'] = registry
return NIL
# sf-begin
def sf_begin(args, env):
if sx_truthy(empty_p(args)):
@@ -1869,7 +1948,7 @@ BOOLEAN_ATTRS = ['async', 'autofocus', 'autoplay', 'checked', 'controls', 'defau
# definition-form?
def is_definition_form(name):
return ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defisland') if sx_truthy((name == 'defisland')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else (name == 'defhandler'))))))
return ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defisland') if sx_truthy((name == 'defisland')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect'))))))))
# parse-element-args
def parse_element_args(args, env):
@@ -1995,7 +2074,7 @@ def render_value_to_html(val, env):
return escape_html(sx_str(val))
# RENDER_HTML_FORMS
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each']
# render-html-form?
def is_render_html_form(name):
@@ -2285,7 +2364,7 @@ def aser_call(name, args, env):
return sx_str('(', join(' ', parts), ')')
# SPECIAL_FORM_NAMES
SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland']
SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland', 'deftype', 'defeffect']
# HO_FORM_NAMES
HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each']
@@ -2379,7 +2458,7 @@ def aser_special(name, expr, env):
elif sx_truthy((name == 'defisland')):
trampoline(eval_expr(expr, env))
return serialize(expr)
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else (name == 'defrelation')))))))))):
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'defrelation') if sx_truthy((name == 'defrelation')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect')))))))))))):
trampoline(eval_expr(expr, env))
return NIL
else:
@@ -3143,7 +3222,7 @@ async def async_map_render(exprs, env, ctx):
return results
# ASYNC_RENDER_FORMS
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each']
# async-render-form?
def async_render_form_p(name):
@@ -3518,7 +3597,7 @@ async def async_aser_call(name, args, env, ctx):
return make_sx_expr(sx_str('(', join(' ', parts), ')'))
# ASYNC_ASER_FORM_NAMES
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland']
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland', 'deftype', 'defeffect']
# ASYNC_ASER_HO_NAMES
ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each']
@@ -3609,7 +3688,7 @@ async def dispatch_async_aser_form(name, expr, env, ctx):
elif sx_truthy((name == 'defisland')):
(await async_eval(expr, env, ctx))
return serialize(expr)
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else (name == 'defaction'))))))))):
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect'))))))))))):
(await async_eval(expr, env, ctx))
return NIL
else: