Add (name :as type) annotation syntax for defcomp params
parse-comp-params now recognizes (name :as type) — a 3-element list with :as keyword separator. Type annotations are stored on the Component via component-param-types and used by types.sx for call-site checking. Unannotated params default to any. 428/428 tests pass (50 types tests including 6 annotation tests). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-11T16:56:11Z";
|
var SX_VERSION = "2026-03-11T17:12:38Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -904,9 +904,13 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
|||||||
var parsed = parseCompParams(paramsRaw);
|
var parsed = parseCompParams(paramsRaw);
|
||||||
var params = first(parsed);
|
var params = first(parsed);
|
||||||
var hasChildren = nth(parsed, 1);
|
var hasChildren = nth(parsed, 1);
|
||||||
|
var paramTypes = nth(parsed, 2);
|
||||||
var affinity = defcompKwarg(args, "affinity", "auto");
|
var affinity = defcompKwarg(args, "affinity", "auto");
|
||||||
return (function() {
|
return (function() {
|
||||||
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
|
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
|
||||||
|
if (isSxTruthy((isSxTruthy(!isSxTruthy(isNil(paramTypes))) && !isSxTruthy(isEmpty(keys(paramTypes)))))) {
|
||||||
|
componentSetParamTypes_b(comp, paramTypes);
|
||||||
|
}
|
||||||
envSet(env, symbolName(nameSym), comp);
|
envSet(env, symbolName(nameSym), comp);
|
||||||
return comp;
|
return comp;
|
||||||
})();
|
})();
|
||||||
@@ -928,15 +932,21 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
|||||||
// parse-comp-params
|
// parse-comp-params
|
||||||
var parseCompParams = function(paramsExpr) { return (function() {
|
var parseCompParams = function(paramsExpr) { return (function() {
|
||||||
var params = [];
|
var params = [];
|
||||||
|
var paramTypes = {};
|
||||||
var hasChildren = false;
|
var hasChildren = false;
|
||||||
var inKey = false;
|
var inKey = false;
|
||||||
{ var _c = paramsExpr; for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; if (isSxTruthy((typeOf(p) == "symbol"))) {
|
{ var _c = paramsExpr; for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; (isSxTruthy((isSxTruthy((typeOf(p) == "list")) && isSxTruthy((len(p) == 3)) && isSxTruthy((typeOf(first(p)) == "symbol")) && isSxTruthy((typeOf(nth(p, 1)) == "keyword")) && (keywordName(nth(p, 1)) == "as"))) ? (function() {
|
||||||
(function() {
|
var name = symbolName(first(p));
|
||||||
|
var ptype = nth(p, 2);
|
||||||
|
return (function() {
|
||||||
|
var typeVal = (isSxTruthy((typeOf(ptype) == "symbol")) ? symbolName(ptype) : ptype);
|
||||||
|
return (isSxTruthy(!isSxTruthy(hasChildren)) ? (append_b(params, name), dictSet(paramTypes, name, typeVal)) : NIL);
|
||||||
|
})();
|
||||||
|
})() : (isSxTruthy((typeOf(p) == "symbol")) ? (function() {
|
||||||
var name = symbolName(p);
|
var name = symbolName(p);
|
||||||
return (isSxTruthy((name == "&key")) ? (inKey = true) : (isSxTruthy((name == "&rest")) ? (hasChildren = true) : (isSxTruthy((name == "&children")) ? (hasChildren = true) : (isSxTruthy(hasChildren) ? NIL : (isSxTruthy(inKey) ? append_b(params, name) : append_b(params, name))))));
|
return (isSxTruthy((name == "&key")) ? (inKey = true) : (isSxTruthy((name == "&rest")) ? (hasChildren = true) : (isSxTruthy((name == "&children")) ? (hasChildren = true) : (isSxTruthy(hasChildren) ? NIL : (isSxTruthy(inKey) ? append_b(params, name) : append_b(params, name))))));
|
||||||
})();
|
})() : NIL)); } }
|
||||||
} } }
|
return [params, hasChildren, paramTypes];
|
||||||
return [params, hasChildren];
|
|
||||||
})(); };
|
})(); };
|
||||||
|
|
||||||
// sf-defisland
|
// sf-defisland
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ class PyEmitter:
|
|||||||
"component-has-children?": "component_has_children",
|
"component-has-children?": "component_has_children",
|
||||||
"component-name": "component_name",
|
"component-name": "component_name",
|
||||||
"component-affinity": "component_affinity",
|
"component-affinity": "component_affinity",
|
||||||
|
"component-param-types": "component_param_types",
|
||||||
|
"component-set-param-types!": "component_set_param_types",
|
||||||
"macro-params": "macro_params",
|
"macro-params": "macro_params",
|
||||||
"macro-rest-param": "macro_rest_param",
|
"macro-rest-param": "macro_rest_param",
|
||||||
"macro-body": "macro_body",
|
"macro-body": "macro_body",
|
||||||
|
|||||||
@@ -518,8 +518,13 @@
|
|||||||
(parsed (parse-comp-params params-raw))
|
(parsed (parse-comp-params params-raw))
|
||||||
(params (first parsed))
|
(params (first parsed))
|
||||||
(has-children (nth parsed 1))
|
(has-children (nth parsed 1))
|
||||||
|
(param-types (nth parsed 2))
|
||||||
(affinity (defcomp-kwarg args "affinity" "auto")))
|
(affinity (defcomp-kwarg args "affinity" "auto")))
|
||||||
(let ((comp (make-component comp-name params has-children body env affinity)))
|
(let ((comp (make-component comp-name params has-children body env affinity)))
|
||||||
|
;; Store type annotations if any were declared
|
||||||
|
(when (and (not (nil? param-types))
|
||||||
|
(not (empty? (keys param-types))))
|
||||||
|
(component-set-param-types! comp param-types))
|
||||||
(env-set! env (symbol-name name-sym) comp)
|
(env-set! env (symbol-name name-sym) comp)
|
||||||
comp))))
|
comp))))
|
||||||
|
|
||||||
@@ -541,24 +546,44 @@
|
|||||||
|
|
||||||
(define parse-comp-params
|
(define parse-comp-params
|
||||||
(fn (params-expr)
|
(fn (params-expr)
|
||||||
;; Parse (&key param1 param2 &children) → (params has-children)
|
;; Parse (&key param1 param2 &children) → (params has-children param-types)
|
||||||
;; Also accepts &rest as synonym for &children.
|
;; Also accepts &rest as synonym for &children.
|
||||||
|
;; Supports typed params: (name :as type) — a 3-element list where
|
||||||
|
;; the second element is the keyword :as. Unannotated params get no
|
||||||
|
;; type entry. param-types is a dict {name → type-expr} or empty dict.
|
||||||
(let ((params (list))
|
(let ((params (list))
|
||||||
|
(param-types (dict))
|
||||||
(has-children false)
|
(has-children false)
|
||||||
(in-key false))
|
(in-key false))
|
||||||
(for-each
|
(for-each
|
||||||
(fn (p)
|
(fn (p)
|
||||||
(when (= (type-of p) "symbol")
|
(if (and (= (type-of p) "list")
|
||||||
(let ((name (symbol-name p)))
|
(= (len p) 3)
|
||||||
(cond
|
(= (type-of (first p)) "symbol")
|
||||||
(= name "&key") (set! in-key true)
|
(= (type-of (nth p 1)) "keyword")
|
||||||
(= name "&rest") (set! has-children true)
|
(= (keyword-name (nth p 1)) "as"))
|
||||||
(= name "&children") (set! has-children true)
|
;; Typed param: (name :as type)
|
||||||
has-children nil ;; skip params after &children/&rest
|
(let ((name (symbol-name (first p)))
|
||||||
in-key (append! params name)
|
(ptype (nth p 2)))
|
||||||
:else (append! params name)))))
|
;; Convert type to string if it's a symbol
|
||||||
|
(let ((type-val (if (= (type-of ptype) "symbol")
|
||||||
|
(symbol-name ptype)
|
||||||
|
ptype)))
|
||||||
|
(when (not has-children)
|
||||||
|
(append! params name)
|
||||||
|
(dict-set! param-types name type-val))))
|
||||||
|
;; Untyped param or marker
|
||||||
|
(when (= (type-of p) "symbol")
|
||||||
|
(let ((name (symbol-name p)))
|
||||||
|
(cond
|
||||||
|
(= name "&key") (set! in-key true)
|
||||||
|
(= name "&rest") (set! has-children true)
|
||||||
|
(= name "&children") (set! has-children true)
|
||||||
|
has-children nil ;; skip params after &children/&rest
|
||||||
|
in-key (append! params name)
|
||||||
|
:else (append! params name))))))
|
||||||
params-expr)
|
params-expr)
|
||||||
(list params has-children))))
|
(list params has-children param-types))))
|
||||||
|
|
||||||
|
|
||||||
(define sf-defisland
|
(define sf-defisland
|
||||||
|
|||||||
@@ -266,6 +266,14 @@ def component_affinity(c):
|
|||||||
return getattr(c, 'affinity', 'auto')
|
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):
|
def macro_params(m):
|
||||||
return m.params
|
return m.params
|
||||||
|
|
||||||
@@ -1510,8 +1518,11 @@ def sf_defcomp(args, env):
|
|||||||
parsed = parse_comp_params(params_raw)
|
parsed = parse_comp_params(params_raw)
|
||||||
params = first(parsed)
|
params = first(parsed)
|
||||||
has_children = nth(parsed, 1)
|
has_children = nth(parsed, 1)
|
||||||
|
param_types = nth(parsed, 2)
|
||||||
affinity = defcomp_kwarg(args, 'affinity', 'auto')
|
affinity = defcomp_kwarg(args, 'affinity', 'auto')
|
||||||
comp = make_component(comp_name, params, has_children, body, env, affinity)
|
comp = make_component(comp_name, params, has_children, body, env, affinity)
|
||||||
|
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)
|
||||||
env[symbol_name(name_sym)] = comp
|
env[symbol_name(name_sym)] = comp
|
||||||
return comp
|
return comp
|
||||||
|
|
||||||
@@ -1530,24 +1541,33 @@ def defcomp_kwarg(args, key, default_):
|
|||||||
def parse_comp_params(params_expr):
|
def parse_comp_params(params_expr):
|
||||||
_cells = {}
|
_cells = {}
|
||||||
params = []
|
params = []
|
||||||
|
param_types = {}
|
||||||
_cells['has_children'] = False
|
_cells['has_children'] = False
|
||||||
_cells['in_key'] = False
|
_cells['in_key'] = False
|
||||||
for p in params_expr:
|
for p in params_expr:
|
||||||
if sx_truthy((type_of(p) == 'symbol')):
|
if sx_truthy(((type_of(p) == 'list') if not sx_truthy((type_of(p) == 'list')) else ((len(p) == 3) if not sx_truthy((len(p) == 3)) else ((type_of(first(p)) == 'symbol') if not sx_truthy((type_of(first(p)) == 'symbol')) else ((type_of(nth(p, 1)) == 'keyword') if not sx_truthy((type_of(nth(p, 1)) == 'keyword')) else (keyword_name(nth(p, 1)) == 'as')))))):
|
||||||
name = symbol_name(p)
|
name = symbol_name(first(p))
|
||||||
if sx_truthy((name == '&key')):
|
ptype = nth(p, 2)
|
||||||
_cells['in_key'] = True
|
type_val = (symbol_name(ptype) if sx_truthy((type_of(ptype) == 'symbol')) else ptype)
|
||||||
elif sx_truthy((name == '&rest')):
|
if sx_truthy((not sx_truthy(_cells['has_children']))):
|
||||||
_cells['has_children'] = True
|
|
||||||
elif sx_truthy((name == '&children')):
|
|
||||||
_cells['has_children'] = True
|
|
||||||
elif sx_truthy(_cells['has_children']):
|
|
||||||
NIL
|
|
||||||
elif sx_truthy(_cells['in_key']):
|
|
||||||
params.append(name)
|
params.append(name)
|
||||||
else:
|
param_types[name] = type_val
|
||||||
params.append(name)
|
else:
|
||||||
return [params, _cells['has_children']]
|
if sx_truthy((type_of(p) == 'symbol')):
|
||||||
|
name = symbol_name(p)
|
||||||
|
if sx_truthy((name == '&key')):
|
||||||
|
_cells['in_key'] = True
|
||||||
|
elif sx_truthy((name == '&rest')):
|
||||||
|
_cells['has_children'] = True
|
||||||
|
elif sx_truthy((name == '&children')):
|
||||||
|
_cells['has_children'] = True
|
||||||
|
elif sx_truthy(_cells['has_children']):
|
||||||
|
NIL
|
||||||
|
elif sx_truthy(_cells['in_key']):
|
||||||
|
params.append(name)
|
||||||
|
else:
|
||||||
|
params.append(name)
|
||||||
|
return [params, _cells['has_children'], param_types]
|
||||||
|
|
||||||
# sf-defisland
|
# sf-defisland
|
||||||
def sf_defisland(args, env):
|
def sf_defisland(args, env):
|
||||||
|
|||||||
@@ -283,3 +283,62 @@
|
|||||||
(dict) (test-prim-types))))
|
(dict) (test-prim-types))))
|
||||||
(assert-true (> (len diagnostics) 0))
|
(assert-true (> (len diagnostics) 0))
|
||||||
(assert-equal "warning" (dict-get (first diagnostics) "level"))))))
|
(assert-equal "warning" (dict-get (first diagnostics) "level"))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Annotation syntax: (name :as type) in defcomp params
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "typed-defcomp"
|
||||||
|
(deftest "typed params are parsed and stored"
|
||||||
|
(let ((env (test-env)))
|
||||||
|
(defcomp ~typed-widget (&key (title :as string) (count :as number)) (div title count))
|
||||||
|
(let ((pt (component-param-types ~typed-widget)))
|
||||||
|
(assert-true (not (nil? pt)))
|
||||||
|
(assert-equal "string" (dict-get pt "title"))
|
||||||
|
(assert-equal "number" (dict-get pt "count")))))
|
||||||
|
|
||||||
|
(deftest "mixed typed and untyped params"
|
||||||
|
(let ((env (test-env)))
|
||||||
|
(defcomp ~mixed-widget (&key (title :as string) subtitle) (div title subtitle))
|
||||||
|
(let ((pt (component-param-types ~mixed-widget)))
|
||||||
|
(assert-true (not (nil? pt)))
|
||||||
|
(assert-equal "string" (dict-get pt "title"))
|
||||||
|
;; subtitle has no annotation — should not be in param-types
|
||||||
|
(assert-false (has-key? pt "subtitle")))))
|
||||||
|
|
||||||
|
(deftest "untyped defcomp has nil param-types"
|
||||||
|
(let ((env (test-env)))
|
||||||
|
(defcomp ~plain-widget (&key title subtitle) (div title subtitle))
|
||||||
|
(assert-true (nil? (component-param-types ~plain-widget)))))
|
||||||
|
|
||||||
|
(deftest "typed component catches type error on call"
|
||||||
|
(let ((env (test-env)))
|
||||||
|
(defcomp ~strict-card (&key (title :as string) (price :as number)) (div title price))
|
||||||
|
;; Call with wrong types
|
||||||
|
(let ((diagnostics
|
||||||
|
(check-component-call "~strict-card" ~strict-card
|
||||||
|
(rest (first (sx-parse "(~strict-card :title 42 :price \"bad\")")))
|
||||||
|
(dict) (test-prim-types))))
|
||||||
|
;; Should have errors for both wrong-type args
|
||||||
|
(assert-true (>= (len diagnostics) 1))
|
||||||
|
(assert-equal "error" (dict-get (first diagnostics) "level")))))
|
||||||
|
|
||||||
|
(deftest "typed component passes correct call"
|
||||||
|
(let ((env (test-env)))
|
||||||
|
(defcomp ~ok-widget (&key (name :as string) (age :as number)) (div name age))
|
||||||
|
(let ((diagnostics
|
||||||
|
(check-component-call "~ok-widget" ~ok-widget
|
||||||
|
(rest (first (sx-parse "(~ok-widget :name \"Alice\" :age 30)")))
|
||||||
|
(dict) (test-prim-types))))
|
||||||
|
(assert-equal 0 (len diagnostics)))))
|
||||||
|
|
||||||
|
(deftest "nullable type accepts nil"
|
||||||
|
(let ((env (test-env)))
|
||||||
|
(defcomp ~nullable-widget (&key (title :as string) (subtitle :as string?)) (div title subtitle))
|
||||||
|
;; Passing nil for nullable param should be fine
|
||||||
|
(let ((diagnostics
|
||||||
|
(check-component-call "~nullable-widget" ~nullable-widget
|
||||||
|
(rest (first (sx-parse "(~nullable-widget :title \"hi\" :subtitle nil)")))
|
||||||
|
(dict) (test-prim-types))))
|
||||||
|
(assert-equal 0 (len diagnostics))))))
|
||||||
|
|||||||
Reference in New Issue
Block a user