Add typed params to 67 primitives, implement check-primitive-call
Annotate all primitives in primitives.sx with (:as type) param types where meaningful (67/80 — 13 polymorphic ops stay untyped). Add parse_primitive_param_types() to boundary_parser.py for extraction. Implement check-primitive-call in types.sx with full positional + rest param validation, thread prim-param-types through check-body-walk, check-component, and check-all. 10 new tests (438 total, all pass). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,83 @@ def parse_primitives_by_module() -> dict[str, frozenset[str]]:
|
||||
return {mod: frozenset(names) for mod, names in modules.items()}
|
||||
|
||||
|
||||
def _parse_param_type(param) -> tuple[str, str | None, bool]:
|
||||
"""Parse a single param entry from a :params list.
|
||||
|
||||
Returns (name, type_or_none, is_rest).
|
||||
A bare symbol like ``x`` → ("x", None, False).
|
||||
A typed form ``(x :as number)`` → ("x", "number", False).
|
||||
The ``&rest`` marker is tracked externally.
|
||||
"""
|
||||
if isinstance(param, Symbol):
|
||||
return (param.name, None, False)
|
||||
if isinstance(param, list) and len(param) == 3:
|
||||
# (name :as type)
|
||||
name_sym, kw, type_val = param
|
||||
if (isinstance(name_sym, Symbol)
|
||||
and isinstance(kw, Keyword) and kw.name == "as"):
|
||||
type_str = type_val.name if isinstance(type_val, Symbol) else str(type_val)
|
||||
return (name_sym.name, type_str, False)
|
||||
return (str(param), None, False)
|
||||
|
||||
|
||||
def parse_primitive_param_types() -> dict[str, dict]:
|
||||
"""Parse primitives.sx and extract param type info for each primitive.
|
||||
|
||||
Returns a dict mapping primitive name to param type descriptor::
|
||||
|
||||
{
|
||||
"+": {"positional": [], "rest_type": "number"},
|
||||
"/": {"positional": [("a", "number"), ("b", "number")], "rest_type": None},
|
||||
"get": {"positional": [("coll", None), ("key", None)], "rest_type": None},
|
||||
}
|
||||
|
||||
Each positional entry is (name, type_or_none). rest_type is the
|
||||
type of the &rest parameter (or None if no &rest, or None if untyped &rest).
|
||||
"""
|
||||
source = _read_file("primitives.sx")
|
||||
exprs = parse_all(source)
|
||||
result: dict[str, dict] = {}
|
||||
|
||||
for expr in exprs:
|
||||
if not isinstance(expr, list) or len(expr) < 2:
|
||||
continue
|
||||
if not isinstance(expr[0], Symbol) or expr[0].name != "define-primitive":
|
||||
continue
|
||||
|
||||
name = expr[1]
|
||||
if not isinstance(name, str):
|
||||
continue
|
||||
|
||||
params_list = _extract_keyword_arg(expr, "params")
|
||||
if not isinstance(params_list, list):
|
||||
continue
|
||||
|
||||
positional: list[tuple[str, str | None]] = []
|
||||
rest_type: str | None = None
|
||||
i = 0
|
||||
while i < len(params_list):
|
||||
item = params_list[i]
|
||||
if isinstance(item, Symbol) and item.name == "&rest":
|
||||
# Next item is the rest param
|
||||
if i + 1 < len(params_list):
|
||||
rname, rtype, _ = _parse_param_type(params_list[i + 1])
|
||||
rest_type = rtype
|
||||
i += 2
|
||||
else:
|
||||
pname, ptype, _ = _parse_param_type(item)
|
||||
if pname != "&rest":
|
||||
positional.append((pname, ptype))
|
||||
i += 1
|
||||
|
||||
# Only store if at least one param has a type
|
||||
has_types = rest_type is not None or any(t is not None for _, t in positional)
|
||||
if has_types:
|
||||
result[name] = {"positional": positional, "rest_type": rest_type}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:
|
||||
"""Parse all boundary sources and return (io_names, {service: helper_names}).
|
||||
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
;; :doc "description"
|
||||
;; :body (reference-implementation ...))
|
||||
;;
|
||||
;; Typed params use (name :as type) syntax:
|
||||
;; (define-primitive "+"
|
||||
;; :params (&rest (args :as number))
|
||||
;; :returns "number"
|
||||
;; :doc "Sum all arguments.")
|
||||
;;
|
||||
;; Untyped params default to `any`. Typed params enable the gradual
|
||||
;; type checker (types.sx) to catch mistyped primitive calls.
|
||||
;;
|
||||
;; The :body is optional — when provided, it gives a reference
|
||||
;; implementation in SX that bootstrap compilers MAY use for testing
|
||||
;; or as a fallback. Most targets will implement natively for performance.
|
||||
@@ -32,89 +41,89 @@
|
||||
(define-module :core.arithmetic)
|
||||
|
||||
(define-primitive "+"
|
||||
:params (&rest args)
|
||||
:params (&rest (args :as number))
|
||||
:returns "number"
|
||||
:doc "Sum all arguments."
|
||||
:body (reduce (fn (a b) (native-add a b)) 0 args))
|
||||
|
||||
(define-primitive "-"
|
||||
:params (a &rest b)
|
||||
:params ((a :as number) &rest (b :as number))
|
||||
:returns "number"
|
||||
:doc "Subtract. Unary: negate. Binary: a - b."
|
||||
:body (if (empty? b) (native-neg a) (native-sub a (first b))))
|
||||
|
||||
(define-primitive "*"
|
||||
:params (&rest args)
|
||||
:params (&rest (args :as number))
|
||||
:returns "number"
|
||||
:doc "Multiply all arguments."
|
||||
:body (reduce (fn (a b) (native-mul a b)) 1 args))
|
||||
|
||||
(define-primitive "/"
|
||||
:params (a b)
|
||||
:params ((a :as number) (b :as number))
|
||||
:returns "number"
|
||||
:doc "Divide a by b."
|
||||
:body (native-div a b))
|
||||
|
||||
(define-primitive "mod"
|
||||
:params (a b)
|
||||
:params ((a :as number) (b :as number))
|
||||
:returns "number"
|
||||
:doc "Modulo a % b."
|
||||
:body (native-mod a b))
|
||||
|
||||
(define-primitive "sqrt"
|
||||
:params (x)
|
||||
:params ((x :as number))
|
||||
:returns "number"
|
||||
:doc "Square root.")
|
||||
|
||||
(define-primitive "pow"
|
||||
:params (x n)
|
||||
:params ((x :as number) (n :as number))
|
||||
:returns "number"
|
||||
:doc "x raised to power n.")
|
||||
|
||||
(define-primitive "abs"
|
||||
:params (x)
|
||||
:params ((x :as number))
|
||||
:returns "number"
|
||||
:doc "Absolute value.")
|
||||
|
||||
(define-primitive "floor"
|
||||
:params (x)
|
||||
:params ((x :as number))
|
||||
:returns "number"
|
||||
:doc "Floor to integer.")
|
||||
|
||||
(define-primitive "ceil"
|
||||
:params (x)
|
||||
:params ((x :as number))
|
||||
:returns "number"
|
||||
:doc "Ceiling to integer.")
|
||||
|
||||
(define-primitive "round"
|
||||
:params (x &rest ndigits)
|
||||
:params ((x :as number) &rest (ndigits :as number))
|
||||
:returns "number"
|
||||
:doc "Round to ndigits decimal places (default 0).")
|
||||
|
||||
(define-primitive "min"
|
||||
:params (&rest args)
|
||||
:params (&rest (args :as number))
|
||||
:returns "number"
|
||||
:doc "Minimum. Single list arg or variadic.")
|
||||
|
||||
(define-primitive "max"
|
||||
:params (&rest args)
|
||||
:params (&rest (args :as number))
|
||||
:returns "number"
|
||||
:doc "Maximum. Single list arg or variadic.")
|
||||
|
||||
(define-primitive "clamp"
|
||||
:params (x lo hi)
|
||||
:params ((x :as number) (lo :as number) (hi :as number))
|
||||
:returns "number"
|
||||
:doc "Clamp x to range [lo, hi]."
|
||||
:body (max lo (min hi x)))
|
||||
|
||||
(define-primitive "inc"
|
||||
:params (n)
|
||||
:params ((n :as number))
|
||||
:returns "number"
|
||||
:doc "Increment by 1."
|
||||
:body (+ n 1))
|
||||
|
||||
(define-primitive "dec"
|
||||
:params (n)
|
||||
:params ((n :as number))
|
||||
:returns "number"
|
||||
:doc "Decrement by 1."
|
||||
:body (- n 1))
|
||||
@@ -159,22 +168,22 @@
|
||||
Same semantics as = but explicit Scheme name.")
|
||||
|
||||
(define-primitive "<"
|
||||
:params (a b)
|
||||
:params ((a :as number) (b :as number))
|
||||
:returns "boolean"
|
||||
:doc "Less than.")
|
||||
|
||||
(define-primitive ">"
|
||||
:params (a b)
|
||||
:params ((a :as number) (b :as number))
|
||||
:returns "boolean"
|
||||
:doc "Greater than.")
|
||||
|
||||
(define-primitive "<="
|
||||
:params (a b)
|
||||
:params ((a :as number) (b :as number))
|
||||
:returns "boolean"
|
||||
:doc "Less than or equal.")
|
||||
|
||||
(define-primitive ">="
|
||||
:params (a b)
|
||||
:params ((a :as number) (b :as number))
|
||||
:returns "boolean"
|
||||
:doc "Greater than or equal.")
|
||||
|
||||
@@ -186,19 +195,19 @@
|
||||
(define-module :core.predicates)
|
||||
|
||||
(define-primitive "odd?"
|
||||
:params (n)
|
||||
:params ((n :as number))
|
||||
:returns "boolean"
|
||||
:doc "True if n is odd."
|
||||
:body (= (mod n 2) 1))
|
||||
|
||||
(define-primitive "even?"
|
||||
:params (n)
|
||||
:params ((n :as number))
|
||||
:returns "boolean"
|
||||
:doc "True if n is even."
|
||||
:body (= (mod n 2) 0))
|
||||
|
||||
(define-primitive "zero?"
|
||||
:params (n)
|
||||
:params ((n :as number))
|
||||
:returns "boolean"
|
||||
:doc "True if n is zero."
|
||||
:body (= n 0))
|
||||
@@ -274,82 +283,82 @@
|
||||
:doc "Concatenate all args as strings. nil → empty string, bool → true/false.")
|
||||
|
||||
(define-primitive "concat"
|
||||
:params (&rest colls)
|
||||
:params (&rest (colls :as list))
|
||||
:returns "list"
|
||||
:doc "Concatenate multiple lists into one. Skips nil values.")
|
||||
|
||||
(define-primitive "upper"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "string"
|
||||
:doc "Uppercase string.")
|
||||
|
||||
(define-primitive "upcase"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "string"
|
||||
:doc "Alias for upper. Uppercase string.")
|
||||
|
||||
(define-primitive "lower"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "string"
|
||||
:doc "Lowercase string.")
|
||||
|
||||
(define-primitive "downcase"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "string"
|
||||
:doc "Alias for lower. Lowercase string.")
|
||||
|
||||
(define-primitive "string-length"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "number"
|
||||
:doc "Length of string in characters.")
|
||||
|
||||
(define-primitive "substring"
|
||||
:params (s start end)
|
||||
:params ((s :as string) (start :as number) (end :as number))
|
||||
:returns "string"
|
||||
:doc "Extract substring from start (inclusive) to end (exclusive).")
|
||||
|
||||
(define-primitive "string-contains?"
|
||||
:params (s needle)
|
||||
:params ((s :as string) (needle :as string))
|
||||
:returns "boolean"
|
||||
:doc "True if string s contains substring needle.")
|
||||
|
||||
(define-primitive "trim"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "string"
|
||||
:doc "Strip leading/trailing whitespace.")
|
||||
|
||||
(define-primitive "split"
|
||||
:params (s &rest sep)
|
||||
:params ((s :as string) &rest (sep :as string))
|
||||
:returns "list"
|
||||
:doc "Split string by separator (default space).")
|
||||
|
||||
(define-primitive "join"
|
||||
:params (sep coll)
|
||||
:params ((sep :as string) (coll :as list))
|
||||
:returns "string"
|
||||
:doc "Join collection items with separator string.")
|
||||
|
||||
(define-primitive "replace"
|
||||
:params (s old new)
|
||||
:params ((s :as string) (old :as string) (new :as string))
|
||||
:returns "string"
|
||||
:doc "Replace all occurrences of old with new in s.")
|
||||
|
||||
(define-primitive "slice"
|
||||
:params (coll start &rest end)
|
||||
:params (coll (start :as number) &rest (end :as number))
|
||||
:returns "any"
|
||||
:doc "Slice a string or list from start to end (exclusive). End is optional.")
|
||||
|
||||
(define-primitive "index-of"
|
||||
:params (s needle &rest from)
|
||||
:params ((s :as string) (needle :as string) &rest (from :as number))
|
||||
:returns "number"
|
||||
:doc "Index of first occurrence of needle in s, or -1 if not found. Optional start index.")
|
||||
|
||||
(define-primitive "starts-with?"
|
||||
:params (s prefix)
|
||||
:params ((s :as string) (prefix :as string))
|
||||
:returns "boolean"
|
||||
:doc "True if string s starts with prefix.")
|
||||
|
||||
(define-primitive "ends-with?"
|
||||
:params (s suffix)
|
||||
:params ((s :as string) (suffix :as string))
|
||||
:returns "boolean"
|
||||
:doc "True if string s ends with suffix.")
|
||||
|
||||
@@ -371,7 +380,7 @@
|
||||
:doc "Create a dict from key/value pairs: (dict :a 1 :b 2).")
|
||||
|
||||
(define-primitive "range"
|
||||
:params (start end &rest step)
|
||||
:params ((start :as number) (end :as number) &rest (step :as number))
|
||||
:returns "list"
|
||||
:doc "Integer range [start, end) with optional step.")
|
||||
|
||||
@@ -386,57 +395,57 @@
|
||||
:doc "Length of string, list, or dict.")
|
||||
|
||||
(define-primitive "first"
|
||||
:params (coll)
|
||||
:params ((coll :as list))
|
||||
:returns "any"
|
||||
:doc "First element, or nil if empty.")
|
||||
|
||||
(define-primitive "last"
|
||||
:params (coll)
|
||||
:params ((coll :as list))
|
||||
:returns "any"
|
||||
:doc "Last element, or nil if empty.")
|
||||
|
||||
(define-primitive "rest"
|
||||
:params (coll)
|
||||
:params ((coll :as list))
|
||||
:returns "list"
|
||||
:doc "All elements except the first.")
|
||||
|
||||
(define-primitive "nth"
|
||||
:params (coll n)
|
||||
:params ((coll :as list) (n :as number))
|
||||
:returns "any"
|
||||
:doc "Element at index n, or nil if out of bounds.")
|
||||
|
||||
(define-primitive "cons"
|
||||
:params (x coll)
|
||||
:params (x (coll :as list))
|
||||
:returns "list"
|
||||
:doc "Prepend x to coll.")
|
||||
|
||||
(define-primitive "append"
|
||||
:params (coll x)
|
||||
:params ((coll :as list) x)
|
||||
:returns "list"
|
||||
:doc "If x is a list, concatenate. Otherwise append x as single element.")
|
||||
|
||||
(define-primitive "append!"
|
||||
:params (coll x)
|
||||
:params ((coll :as list) x)
|
||||
:returns "list"
|
||||
:doc "Mutate coll by appending x in-place. Returns coll.")
|
||||
|
||||
(define-primitive "reverse"
|
||||
:params (coll)
|
||||
:params ((coll :as list))
|
||||
:returns "list"
|
||||
:doc "Return coll in reverse order.")
|
||||
|
||||
(define-primitive "flatten"
|
||||
:params (coll)
|
||||
:params ((coll :as list))
|
||||
:returns "list"
|
||||
:doc "Flatten one level of nesting. Nested lists become top-level elements.")
|
||||
|
||||
(define-primitive "chunk-every"
|
||||
:params (coll n)
|
||||
:params ((coll :as list) (n :as number))
|
||||
:returns "list"
|
||||
:doc "Split coll into sub-lists of size n.")
|
||||
|
||||
(define-primitive "zip-pairs"
|
||||
:params (coll)
|
||||
:params ((coll :as list))
|
||||
:returns "list"
|
||||
:doc "Consecutive pairs: (1 2 3 4) → ((1 2) (2 3) (3 4)).")
|
||||
|
||||
@@ -448,37 +457,37 @@
|
||||
(define-module :core.dict)
|
||||
|
||||
(define-primitive "keys"
|
||||
:params (d)
|
||||
:params ((d :as dict))
|
||||
:returns "list"
|
||||
:doc "List of dict keys.")
|
||||
|
||||
(define-primitive "vals"
|
||||
:params (d)
|
||||
:params ((d :as dict))
|
||||
:returns "list"
|
||||
:doc "List of dict values.")
|
||||
|
||||
(define-primitive "merge"
|
||||
:params (&rest dicts)
|
||||
:params (&rest (dicts :as dict))
|
||||
:returns "dict"
|
||||
:doc "Merge dicts left to right. Later keys win. Skips nil.")
|
||||
|
||||
(define-primitive "has-key?"
|
||||
:params (d key)
|
||||
:params ((d :as dict) key)
|
||||
:returns "boolean"
|
||||
:doc "True if dict d contains key.")
|
||||
|
||||
(define-primitive "assoc"
|
||||
:params (d &rest pairs)
|
||||
:params ((d :as dict) &rest pairs)
|
||||
:returns "dict"
|
||||
:doc "Return new dict with key/value pairs added/overwritten.")
|
||||
|
||||
(define-primitive "dissoc"
|
||||
:params (d &rest keys)
|
||||
:params ((d :as dict) &rest keys)
|
||||
:returns "dict"
|
||||
:doc "Return new dict with keys removed.")
|
||||
|
||||
(define-primitive "dict-set!"
|
||||
:params (d key val)
|
||||
:params ((d :as dict) key val)
|
||||
:returns "any"
|
||||
:doc "Mutate dict d by setting key to val in-place. Returns val.")
|
||||
|
||||
@@ -495,12 +504,12 @@
|
||||
(define-module :stdlib.format)
|
||||
|
||||
(define-primitive "format-date"
|
||||
:params (date-str fmt)
|
||||
:params ((date-str :as string) (fmt :as string))
|
||||
:returns "string"
|
||||
:doc "Parse ISO date string and format with strftime-style format.")
|
||||
|
||||
(define-primitive "format-decimal"
|
||||
:params (val &rest places)
|
||||
:params ((val :as number) &rest (places :as number))
|
||||
:returns "string"
|
||||
:doc "Format number with fixed decimal places (default 2).")
|
||||
|
||||
@@ -510,7 +519,7 @@
|
||||
:doc "Parse string to integer with optional default on failure.")
|
||||
|
||||
(define-primitive "parse-datetime"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "string"
|
||||
:doc "Parse datetime string — identity passthrough (returns string or nil).")
|
||||
|
||||
@@ -522,17 +531,17 @@
|
||||
(define-module :stdlib.text)
|
||||
|
||||
(define-primitive "pluralize"
|
||||
:params (count &rest forms)
|
||||
:params ((count :as number) &rest (forms :as string))
|
||||
:returns "string"
|
||||
:doc "Pluralize: (pluralize 1) → \"\", (pluralize 2) → \"s\". Or (pluralize n \"item\" \"items\").")
|
||||
|
||||
(define-primitive "escape"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "string"
|
||||
:doc "HTML-escape a string (&, <, >, \", ').")
|
||||
|
||||
(define-primitive "strip-tags"
|
||||
:params (s)
|
||||
:params ((s :as string))
|
||||
:returns "string"
|
||||
:doc "Remove HTML tags from string.")
|
||||
|
||||
@@ -567,16 +576,16 @@
|
||||
:doc "Return type name: number, string, boolean, nil, symbol, keyword, list, dict, lambda, component, island, macro.")
|
||||
|
||||
(define-primitive "symbol-name"
|
||||
:params (sym)
|
||||
:params ((sym :as symbol))
|
||||
:returns "string"
|
||||
:doc "Return the name string of a symbol.")
|
||||
|
||||
(define-primitive "keyword-name"
|
||||
:params (kw)
|
||||
:params ((kw :as keyword))
|
||||
:returns "string"
|
||||
:doc "Return the name string of a keyword.")
|
||||
|
||||
(define-primitive "sx-parse"
|
||||
:params (source)
|
||||
:params ((source :as string))
|
||||
:returns "list"
|
||||
:doc "Parse SX source string into a list of AST expressions.")
|
||||
|
||||
@@ -342,3 +342,91 @@
|
||||
(rest (first (sx-parse "(~nullable-widget :title \"hi\" :subtitle nil)")))
|
||||
(dict) (test-prim-types))))
|
||||
(assert-equal 0 (len diagnostics))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Primitive call checking (Phase 5)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "check-primitive-calls"
|
||||
(deftest "correct types produce no errors"
|
||||
(let ((ppt (test-prim-param-types)))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "+" (rest (first (sx-parse "(+ 1 2 3)")))
|
||||
(dict) (test-prim-types) ppt nil)))
|
||||
(assert-equal 0 (len diagnostics)))))
|
||||
|
||||
(deftest "string arg to numeric primitive produces error"
|
||||
(let ((ppt (test-prim-param-types)))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "+" (rest (first (sx-parse "(+ 1 \"hello\")")))
|
||||
(dict) (test-prim-types) ppt nil)))
|
||||
(assert-true (> (len diagnostics) 0))
|
||||
(assert-equal "error" (get (first diagnostics) "level")))))
|
||||
|
||||
(deftest "number arg to string primitive produces error"
|
||||
(let ((ppt (test-prim-param-types)))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "upper" (rest (first (sx-parse "(upper 42)")))
|
||||
(dict) (test-prim-types) ppt nil)))
|
||||
(assert-true (> (len diagnostics) 0))
|
||||
(assert-equal "error" (get (first diagnostics) "level")))))
|
||||
|
||||
(deftest "positional and rest params both checked"
|
||||
;; (- "bad" 1) — first positional arg is string, expects number
|
||||
(let ((ppt (test-prim-param-types)))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "-" (rest (first (sx-parse "(- \"bad\" 1)")))
|
||||
(dict) (test-prim-types) ppt nil)))
|
||||
(assert-true (> (len diagnostics) 0)))))
|
||||
|
||||
(deftest "dict arg to keys is valid"
|
||||
(let ((ppt (test-prim-param-types)))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "keys" (rest (first (sx-parse "(keys {:a 1})")))
|
||||
(dict) (test-prim-types) ppt nil)))
|
||||
(assert-equal 0 (len diagnostics)))))
|
||||
|
||||
(deftest "number arg to keys produces error"
|
||||
(let ((ppt (test-prim-param-types)))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "keys" (rest (first (sx-parse "(keys 42)")))
|
||||
(dict) (test-prim-types) ppt nil)))
|
||||
(assert-true (> (len diagnostics) 0)))))
|
||||
|
||||
(deftest "variable with known type passes check"
|
||||
;; Variable n is known to be number in type-env
|
||||
(let ((ppt (test-prim-param-types))
|
||||
(tenv {"n" "number"}))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "inc" (rest (first (sx-parse "(inc n)")))
|
||||
tenv (test-prim-types) ppt nil)))
|
||||
(assert-equal 0 (len diagnostics)))))
|
||||
|
||||
(deftest "variable with wrong type fails check"
|
||||
;; Variable s is known to be string in type-env
|
||||
(let ((ppt (test-prim-param-types))
|
||||
(tenv {"s" "string"}))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "inc" (rest (first (sx-parse "(inc s)")))
|
||||
tenv (test-prim-types) ppt nil)))
|
||||
(assert-true (> (len diagnostics) 0)))))
|
||||
|
||||
(deftest "any-typed variable skips check"
|
||||
;; Variable x has type any — should not produce errors
|
||||
(let ((ppt (test-prim-param-types))
|
||||
(tenv {"x" "any"}))
|
||||
(let ((diagnostics
|
||||
(check-primitive-call "upper" (rest (first (sx-parse "(upper x)")))
|
||||
tenv (test-prim-types) ppt nil)))
|
||||
(assert-equal 0 (len diagnostics)))))
|
||||
|
||||
(deftest "body-walk catches primitive errors in component"
|
||||
;; Manually build a component and check it via check-body-walk directly
|
||||
(let ((ppt (test-prim-param-types))
|
||||
(body (first (sx-parse "(div (+ name 1))")))
|
||||
(type-env {"name" "string"})
|
||||
(diagnostics (list)))
|
||||
(check-body-walk body "~bad-math" type-env (test-prim-types) ppt (test-env) diagnostics)
|
||||
(assert-true (> (len diagnostics) 0))
|
||||
(assert-equal "error" (get (first diagnostics) "level")))))
|
||||
|
||||
@@ -371,12 +371,49 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define check-primitive-call
|
||||
(fn (name args type-env prim-types)
|
||||
;; Check a primitive call site. Returns list of diagnostics.
|
||||
(fn (name args type-env prim-types prim-param-types comp-name)
|
||||
;; Check a primitive call site against declared param types.
|
||||
;; prim-param-types is a dict: {prim-name → {:positional [...] :rest-type type-or-nil}}
|
||||
;; Each positional entry is a list (name type-or-nil).
|
||||
;; 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.
|
||||
(when (and (not (nil? prim-param-types))
|
||||
(dict-has? prim-param-types name))
|
||||
(let ((sig (get prim-param-types name))
|
||||
(positional (get sig "positional"))
|
||||
(rest-type (get sig "rest-type")))
|
||||
;; Check each positional arg
|
||||
(for-each
|
||||
(fn (idx)
|
||||
(when (< idx (len args))
|
||||
(if (< idx (len positional))
|
||||
;; Positional param — check against declared type
|
||||
(let ((param-info (nth positional idx))
|
||||
(arg-expr (nth args idx)))
|
||||
(let ((expected-type (nth param-info 1)))
|
||||
(when (not (nil? expected-type))
|
||||
(let ((actual (infer-type arg-expr type-env prim-types)))
|
||||
(when (and (not (type-any? expected-type))
|
||||
(not (type-any? actual))
|
||||
(not (subtype? actual expected-type)))
|
||||
(append! diagnostics
|
||||
(make-diagnostic "error"
|
||||
(str "Argument " (+ idx 1) " of `" name
|
||||
"` expects " expected-type ", got " actual)
|
||||
comp-name arg-expr)))))))
|
||||
;; Rest param — check against rest-type
|
||||
(when (not (nil? rest-type))
|
||||
(let ((arg-expr (nth args idx))
|
||||
(actual (infer-type arg-expr type-env prim-types)))
|
||||
(when (and (not (type-any? rest-type))
|
||||
(not (type-any? actual))
|
||||
(not (subtype? actual rest-type)))
|
||||
(append! diagnostics
|
||||
(make-diagnostic "error"
|
||||
(str "Argument " (+ idx 1) " of `" name
|
||||
"` expects " rest-type ", got " actual)
|
||||
comp-name arg-expr))))))))
|
||||
(range 0 (len args) 1))))
|
||||
diagnostics)))
|
||||
|
||||
|
||||
@@ -445,14 +482,15 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define check-body-walk
|
||||
(fn (node comp-name type-env prim-types env diagnostics)
|
||||
(fn (node comp-name type-env prim-types prim-param-types env diagnostics)
|
||||
;; Recursively walk an AST and collect diagnostics.
|
||||
;; prim-param-types: dict of {name → {:positional [...] :rest-type t}} or nil
|
||||
(let ((kind (type-of node)))
|
||||
(when (= kind "list")
|
||||
(when (not (empty? node))
|
||||
(let ((head (first node))
|
||||
(args (rest node)))
|
||||
;; Check component calls
|
||||
;; Check calls when head is a symbol
|
||||
(when (= (type-of head) "symbol")
|
||||
(let ((name (symbol-name head)))
|
||||
;; Component call
|
||||
@@ -464,6 +502,15 @@
|
||||
(check-component-call name comp-val args
|
||||
type-env prim-types)))))
|
||||
|
||||
;; Primitive call — check param types
|
||||
(when (and (not (starts-with? name "~"))
|
||||
(not (nil? prim-param-types))
|
||||
(dict-has? prim-param-types name))
|
||||
(for-each
|
||||
(fn (d) (append! diagnostics d))
|
||||
(check-primitive-call name args type-env prim-types
|
||||
prim-param-types comp-name)))
|
||||
|
||||
;; Recurse into let with extended type env
|
||||
(when (or (= name "let") (= name "let*"))
|
||||
(when (>= (len args) 2)
|
||||
@@ -482,7 +529,7 @@
|
||||
bindings)
|
||||
(for-each
|
||||
(fn (body)
|
||||
(check-body-walk body comp-name extended prim-types env diagnostics))
|
||||
(check-body-walk body comp-name extended prim-types prim-param-types env diagnostics))
|
||||
body-exprs))))
|
||||
|
||||
;; Recurse into define with type binding
|
||||
@@ -495,12 +542,12 @@
|
||||
(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))))))
|
||||
(check-body-walk def-val comp-name type-env prim-types prim-param-types env diagnostics))))))
|
||||
|
||||
;; Recurse into all child expressions
|
||||
(for-each
|
||||
(fn (child)
|
||||
(check-body-walk child comp-name type-env prim-types env diagnostics))
|
||||
(check-body-walk child comp-name type-env prim-types prim-param-types env diagnostics))
|
||||
args)))))))
|
||||
|
||||
|
||||
@@ -509,8 +556,9 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define check-component
|
||||
(fn (comp-name env prim-types)
|
||||
(fn (comp-name env prim-types prim-param-types)
|
||||
;; Type-check a component's body. Returns list of diagnostics.
|
||||
;; prim-param-types: dict of param type info, or nil to skip primitive checking.
|
||||
(let ((comp (env-get env comp-name))
|
||||
(diagnostics (list)))
|
||||
(when (= (type-of comp) "component")
|
||||
@@ -532,7 +580,7 @@
|
||||
(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)))
|
||||
(check-body-walk body comp-name type-env prim-types prim-param-types env diagnostics)))
|
||||
diagnostics)))
|
||||
|
||||
|
||||
@@ -541,8 +589,9 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define check-all
|
||||
(fn (env prim-types)
|
||||
(fn (env prim-types prim-param-types)
|
||||
;; Type-check every component in the environment.
|
||||
;; prim-param-types: dict of param type info, or nil to skip primitive checking.
|
||||
;; Returns list of all diagnostics.
|
||||
(let ((all-diagnostics (list)))
|
||||
(for-each
|
||||
@@ -551,7 +600,7 @@
|
||||
(when (= (type-of val) "component")
|
||||
(for-each
|
||||
(fn (d) (append! all-diagnostics d))
|
||||
(check-component name env prim-types)))))
|
||||
(check-component name env prim-types prim-param-types)))))
|
||||
(keys env))
|
||||
all-diagnostics)))
|
||||
|
||||
@@ -598,4 +647,11 @@
|
||||
;; (component-set-param-types! c d) → store param types on component
|
||||
;; (merge d1 d2) → new dict merging d1 and d2
|
||||
;;
|
||||
;; Primitive param types:
|
||||
;; The host provides prim-param-types as a dict mapping primitive names
|
||||
;; to param type descriptors. Each descriptor is a dict:
|
||||
;; {"positional" [["name" "type-or-nil"] ...] "rest-type" "type-or-nil"}
|
||||
;; Built by boundary_parser.parse_primitive_param_types() in Python.
|
||||
;; Passed to check-component/check-all as an optional extra argument.
|
||||
;;
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -793,8 +793,95 @@ def _load_types(env):
|
||||
}
|
||||
|
||||
env["test-prim-types"] = _test_prim_types
|
||||
|
||||
# test-prim-param-types: param type signatures for primitive call checking
|
||||
def _test_prim_param_types():
|
||||
# Each entry: {"positional": [["name", "type"|None], ...], "rest-type": "type"|None}
|
||||
return {
|
||||
"+": {"positional": [], "rest-type": "number"},
|
||||
"-": {"positional": [["a", "number"]], "rest-type": "number"},
|
||||
"*": {"positional": [], "rest-type": "number"},
|
||||
"/": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
|
||||
"mod": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
|
||||
"sqrt": {"positional": [["x", "number"]], "rest-type": None},
|
||||
"pow": {"positional": [["x", "number"], ["n", "number"]], "rest-type": None},
|
||||
"abs": {"positional": [["x", "number"]], "rest-type": None},
|
||||
"floor": {"positional": [["x", "number"]], "rest-type": None},
|
||||
"ceil": {"positional": [["x", "number"]], "rest-type": None},
|
||||
"round": {"positional": [["x", "number"]], "rest-type": "number"},
|
||||
"min": {"positional": [], "rest-type": "number"},
|
||||
"max": {"positional": [], "rest-type": "number"},
|
||||
"clamp": {"positional": [["x", "number"], ["lo", "number"], ["hi", "number"]], "rest-type": None},
|
||||
"inc": {"positional": [["n", "number"]], "rest-type": None},
|
||||
"dec": {"positional": [["n", "number"]], "rest-type": None},
|
||||
"<": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
|
||||
">": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
|
||||
"<=": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
|
||||
">=": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
|
||||
"odd?": {"positional": [["n", "number"]], "rest-type": None},
|
||||
"even?": {"positional": [["n", "number"]], "rest-type": None},
|
||||
"zero?": {"positional": [["n", "number"]], "rest-type": None},
|
||||
"upper": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"upcase": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"lower": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"downcase": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"string-length": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"substring": {"positional": [["s", "string"], ["start", "number"], ["end", "number"]], "rest-type": None},
|
||||
"string-contains?": {"positional": [["s", "string"], ["needle", "string"]], "rest-type": None},
|
||||
"trim": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"split": {"positional": [["s", "string"]], "rest-type": "string"},
|
||||
"join": {"positional": [["sep", "string"], ["coll", "list"]], "rest-type": None},
|
||||
"replace": {"positional": [["s", "string"], ["old", "string"], ["new", "string"]], "rest-type": None},
|
||||
"index-of": {"positional": [["s", "string"], ["needle", "string"]], "rest-type": "number"},
|
||||
"starts-with?": {"positional": [["s", "string"], ["prefix", "string"]], "rest-type": None},
|
||||
"ends-with?": {"positional": [["s", "string"], ["suffix", "string"]], "rest-type": None},
|
||||
"concat": {"positional": [], "rest-type": "list"},
|
||||
"range": {"positional": [["start", "number"], ["end", "number"]], "rest-type": "number"},
|
||||
"first": {"positional": [["coll", "list"]], "rest-type": None},
|
||||
"last": {"positional": [["coll", "list"]], "rest-type": None},
|
||||
"rest": {"positional": [["coll", "list"]], "rest-type": None},
|
||||
"nth": {"positional": [["coll", "list"], ["n", "number"]], "rest-type": None},
|
||||
"cons": {"positional": [["x", None], ["coll", "list"]], "rest-type": None},
|
||||
"append": {"positional": [["coll", "list"]], "rest-type": None},
|
||||
"append!": {"positional": [["coll", "list"]], "rest-type": None},
|
||||
"reverse": {"positional": [["coll", "list"]], "rest-type": None},
|
||||
"flatten": {"positional": [["coll", "list"]], "rest-type": None},
|
||||
"chunk-every": {"positional": [["coll", "list"], ["n", "number"]], "rest-type": None},
|
||||
"zip-pairs": {"positional": [["coll", "list"]], "rest-type": None},
|
||||
"keys": {"positional": [["d", "dict"]], "rest-type": None},
|
||||
"vals": {"positional": [["d", "dict"]], "rest-type": None},
|
||||
"merge": {"positional": [], "rest-type": "dict"},
|
||||
"has-key?": {"positional": [["d", "dict"]], "rest-type": None},
|
||||
"assoc": {"positional": [["d", "dict"]], "rest-type": None},
|
||||
"dissoc": {"positional": [["d", "dict"]], "rest-type": None},
|
||||
"dict-set!": {"positional": [["d", "dict"]], "rest-type": None},
|
||||
"format-date": {"positional": [["date-str", "string"], ["fmt", "string"]], "rest-type": None},
|
||||
"format-decimal": {"positional": [["val", "number"]], "rest-type": "number"},
|
||||
"parse-datetime": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"pluralize": {"positional": [["count", "number"]], "rest-type": "string"},
|
||||
"escape": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"strip-tags": {"positional": [["s", "string"]], "rest-type": None},
|
||||
"symbol-name": {"positional": [["sym", "symbol"]], "rest-type": None},
|
||||
"keyword-name": {"positional": [["kw", "keyword"]], "rest-type": None},
|
||||
"sx-parse": {"positional": [["source", "string"]], "rest-type": None},
|
||||
}
|
||||
|
||||
env["test-prim-param-types"] = _test_prim_param_types
|
||||
env["test-env"] = lambda: env
|
||||
|
||||
# Platform functions needed by types.sx check-body-walk
|
||||
if "env-get" not in env:
|
||||
env["env-get"] = lambda e, k: e.get(k) if hasattr(e, 'get') else None
|
||||
if "env-has?" not in env:
|
||||
env["env-has?"] = lambda e, k: k in e
|
||||
if "dict-has?" not in env:
|
||||
env["dict-has?"] = lambda d, k: k in d if isinstance(d, dict) else False
|
||||
if "dict-get" not in env:
|
||||
env["dict-get"] = lambda d, k, *default: d.get(k, default[0] if default else None) if isinstance(d, dict) else (default[0] if default else None)
|
||||
# types.sx uses component-has-children (no ?), test runner has component-has-children?
|
||||
if "component-has-children" not in env:
|
||||
env["component-has-children"] = lambda c: getattr(c, 'has_children', False)
|
||||
|
||||
# Try bootstrapped types first, fall back to eval
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import (
|
||||
|
||||
Reference in New Issue
Block a user