From 5d5512e74ad49c1bafae0f92b44b96f4e754cacf Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 11 Mar 2026 18:39:20 +0000 Subject: [PATCH] Add typed params to 67 primitives, implement check-primitive-call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- shared/sx/ref/boundary_parser.py | 77 +++++++++++++++++ shared/sx/ref/primitives.sx | 143 ++++++++++++++++--------------- shared/sx/ref/test-types.sx | 88 +++++++++++++++++++ shared/sx/ref/types.sx | 84 +++++++++++++++--- shared/sx/tests/run.py | 87 +++++++++++++++++++ 5 files changed, 398 insertions(+), 81 deletions(-) diff --git a/shared/sx/ref/boundary_parser.py b/shared/sx/ref/boundary_parser.py index 20b924d..ee4d21f 100644 --- a/shared/sx/ref/boundary_parser.py +++ b/shared/sx/ref/boundary_parser.py @@ -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}). diff --git a/shared/sx/ref/primitives.sx b/shared/sx/ref/primitives.sx index 21fd182..fb19207 100644 --- a/shared/sx/ref/primitives.sx +++ b/shared/sx/ref/primitives.sx @@ -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.") diff --git a/shared/sx/ref/test-types.sx b/shared/sx/ref/test-types.sx index 57fcae7..121ba23 100644 --- a/shared/sx/ref/test-types.sx +++ b/shared/sx/ref/test-types.sx @@ -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"))))) diff --git a/shared/sx/ref/types.sx b/shared/sx/ref/types.sx index 87f2d35..9a9d60f 100644 --- a/shared/sx/ref/types.sx +++ b/shared/sx/ref/types.sx @@ -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. +;; ;; -------------------------------------------------------------------------- diff --git a/shared/sx/tests/run.py b/shared/sx/tests/run.py index eaca44b..c7ccb77 100644 --- a/shared/sx/tests/run.py +++ b/shared/sx/tests/run.py @@ -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 (