Skip to main content

runmat_runtime/builtins/close/
mod.rs

1//! Canonical `close` builtin dispatcher.
2//!
3//! This module owns the single runtime registration for `close` and routes
4//! requests to plotting or networking close handlers.
5
6use runmat_builtins::{
7    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
8    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
9    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
10    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
11    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
12    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
13};
14use runmat_macros::runtime_builtin;
15use runmat_value::Value;
16
17const CLOSE_OUTPUT_RESULT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
18    name: "result",
19    ty: BuiltinParamType::NumericScalar,
20    arity: BuiltinParamArity::Required,
21    default: None,
22    description: "Scalar double status: 1 when the requested close operation completes, 0 when it is refused.",
23}];
24
25const CLOSE_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];
26const CLOSE_INPUTS_TARGET: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27    name: "target",
28    ty: BuiltinParamType::Any,
29    arity: BuiltinParamArity::Required,
30    default: None,
31    description: "Figure target, tcp resource handle, option token, or target container.",
32}];
33const CLOSE_INPUTS_TARGETS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
34    name: "targets",
35    ty: BuiltinParamType::Any,
36    arity: BuiltinParamArity::Variadic,
37    default: None,
38    description: "One or more close targets.",
39}];
40
41const CLOSE_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
42    BuiltinSignatureDescriptor {
43        label: "result = close()",
44        inputs: &CLOSE_INPUTS_NONE,
45        outputs: &CLOSE_OUTPUT_RESULT,
46    },
47    BuiltinSignatureDescriptor {
48        label: "result = close(target)",
49        inputs: &CLOSE_INPUTS_TARGET,
50        outputs: &CLOSE_OUTPUT_RESULT,
51    },
52    BuiltinSignatureDescriptor {
53        label: "result = close(targets...)",
54        inputs: &CLOSE_INPUTS_TARGETS,
55        outputs: &CLOSE_OUTPUT_RESULT,
56    },
57];
58
59const CLOSE_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
60    code: "RM.CLOSE.INVALID_ARGUMENT",
61    identifier: Some("RunMat:close:InvalidArgument"),
62    when: "Close target values are invalid or unsupported.",
63    message: "close: invalid argument",
64};
65const CLOSE_ERROR_INVALID_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
66    code: "RM.CLOSE.INVALID_HANDLE",
67    identifier: Some("RunMat:close:InvalidHandle"),
68    when: "A structure target is not a valid networking resource handle.",
69    message: "close: invalid handle",
70};
71const CLOSE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
72    code: "RM.CLOSE.INTERNAL",
73    identifier: None,
74    when: "Internal networking, gather, or plotting close processing fails.",
75    message: "close: internal error",
76};
77const CLOSE_ERRORS: [BuiltinErrorDescriptor; 3] = [
78    CLOSE_ERROR_INVALID_ARGUMENT,
79    CLOSE_ERROR_INVALID_HANDLE,
80    CLOSE_ERROR_INTERNAL,
81];
82
83pub const CLOSE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
84    signatures: &CLOSE_SIGNATURES,
85    output_mode: BuiltinOutputMode::Fixed,
86    completion_policy: BuiltinCompletionPolicy::Public,
87    errors: &CLOSE_ERRORS,
88};
89
90pub(crate) const CLOSE_INTEGER_FIGURE_NUMBER_EXTENSION: BuiltinExtensionDescriptor =
91    BuiltinExtensionDescriptor {
92        id: "close-integer-figure-number",
93        mode: BuiltinExtensionMode::RunMatOnly,
94        description: "close with a typed integer figure number is a RunMat extension",
95        error_identifier: Some("RunMat:compatibility:CloseIntegerFigureNumberExtension"),
96    };
97
98pub(crate) const CLOSE_VARIADIC_TARGETS_EXTENSION: BuiltinExtensionDescriptor =
99    BuiltinExtensionDescriptor {
100        id: "close-variadic-targets",
101        mode: BuiltinExtensionMode::RunMatOnly,
102        description: "close with separate variadic targets is a RunMat extension",
103        error_identifier: Some("RunMat:compatibility:CloseVariadicTargetsExtension"),
104    };
105
106pub const CLOSE_EXTENSIONS: [BuiltinExtensionDescriptor; 2] = [
107    CLOSE_INTEGER_FIGURE_NUMBER_EXTENSION,
108    CLOSE_VARIADIC_TARGETS_EXTENSION,
109];
110
111const CLOSE_INTEGER_TARGET_INPUTS: [BuiltinIntegerInputCapability; 1] =
112    [BuiltinIntegerInputCapability {
113        name: "fig",
114        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
115        availability: BuiltinIntegerInputAvailability::RunMatOnly,
116        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
117        notes: "RunMat mode interprets a scalar or array of any built-in integer class as figure numbers. The public compatibility contract documents figure numbers but does not advertise typed integer classes.",
118    }];
119
120pub const CLOSE_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
121    [BuiltinIntegerCapabilityDescriptor {
122        form: "status = close(integer_fig)",
123        inputs: &CLOSE_INTEGER_TARGET_INPUTS,
124        computation_domain: BuiltinIntegerComputationDomain::Structural,
125        output_class: BuiltinIntegerOutputClassRule::Double,
126        overflow: BuiltinIntegerOverflowRule::NotApplicable,
127        backend: BuiltinIntegerBackendRule::HostOnly,
128        overload: BuiltinIntegerOverloadKind::StructuralParameter,
129        notes: "Figure numbers are read from authoritative host integer storage and must be positive and representable as RunMat's u32 figure identifier. Resident numeric targets are rejected before networking/provider gather. Successful and no-op plotting closures return scalar double 1; callback-driven refusal remains unavailable until CloseRequestFcn is implemented.",
130    }];
131
132#[runtime_builtin(
133    name = "close",
134    category = "general",
135    summary = "Close figures or networking resources.",
136    keywords = "close,figure,tcpclient,tcpserver,networking",
137    sink = true,
138    suppress_auto_output = true,
139    type_resolver(crate::builtins::io::type_resolvers::close_type),
140    descriptor(crate::builtins::close::CLOSE_DESCRIPTOR),
141    extensions(crate::builtins::close::CLOSE_EXTENSIONS),
142    integer_capabilities(crate::builtins::close::CLOSE_INTEGER_CAPABILITIES),
143    builtin_path = "crate::builtins::close"
144)]
145pub async fn close_builtin(args: Vec<Value>) -> crate::BuiltinResult<f64> {
146    if args.len() > 1 {
147        crate::compatibility::ensure_builtin_extension_enabled(
148            &CLOSE_VARIADIC_TARGETS_EXTENSION,
149            "close",
150        )?;
151    }
152    if args.iter().any(is_typed_integer_value) {
153        crate::compatibility::ensure_builtin_extension_enabled(
154            &CLOSE_INTEGER_FIGURE_NUMBER_EXTENSION,
155            "close",
156        )?;
157    }
158    if let Some(status) = crate::builtins::io::net::close::close_if_network_targets(&args).await? {
159        return Ok(status);
160    }
161
162    close_plotting_targets(&args)
163}
164
165fn is_typed_integer_value(value: &Value) -> bool {
166    matches!(value, Value::Int(_))
167        || matches!(value, Value::Tensor(tensor) if tensor.integer_storage().is_some())
168}
169
170#[cfg(feature = "plot-core")]
171fn close_plotting_targets(args: &[Value]) -> crate::BuiltinResult<f64> {
172    crate::builtins::plotting::close::close_plot_targets(args)
173}
174
175#[cfg(not(feature = "plot-core"))]
176fn close_plotting_targets(_args: &[Value]) -> crate::BuiltinResult<f64> {
177    let mut builder =
178        crate::build_runtime_error(CLOSE_ERROR_INVALID_ARGUMENT.message).with_builtin("close");
179    if let Some(identifier) = CLOSE_ERROR_INVALID_ARGUMENT.identifier {
180        builder = builder.with_identifier(identifier);
181    }
182    Err(builder.build())
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use runmat_value::{IntegerStorage, Tensor};
189
190    #[test]
191    fn compatibility_mode_rejects_all_integer_figure_number_classes() {
192        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
193        let storages = [
194            IntegerStorage::I8(vec![1]),
195            IntegerStorage::I16(vec![1]),
196            IntegerStorage::I32(vec![1]),
197            IntegerStorage::I64(vec![1]),
198            IntegerStorage::U8(vec![1]),
199            IntegerStorage::U16(vec![1]),
200            IntegerStorage::U32(vec![1]),
201            IntegerStorage::U64(vec![1]),
202        ];
203
204        for storage in storages {
205            let value = Value::Tensor(Tensor::new_integer(storage, vec![1, 1]).expect("figure"));
206            let err = futures::executor::block_on(close_builtin(vec![value]))
207                .expect_err("typed integer extension must be gated");
208            assert_eq!(
209                err.identifier(),
210                Some("RunMat:compatibility:CloseIntegerFigureNumberExtension")
211            );
212        }
213    }
214
215    #[test]
216    fn close_integer_capability_is_structural_and_host_only() {
217        let capability = &CLOSE_INTEGER_CAPABILITIES[0];
218        assert_eq!(capability.inputs[0].classes.len(), 8);
219        assert_eq!(
220            capability.computation_domain,
221            BuiltinIntegerComputationDomain::Structural
222        );
223        assert_eq!(capability.backend, BuiltinIntegerBackendRule::HostOnly);
224    }
225
226    #[test]
227    fn compatibility_mode_rejects_separate_variadic_targets_before_dispatch() {
228        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
229        let err = futures::executor::block_on(close_builtin(vec![
230            Value::String("clients".into()),
231            Value::String("servers".into()),
232        ]))
233        .expect_err("RunMat-only variadic close targets");
234        assert_eq!(
235            err.identifier(),
236            CLOSE_VARIADIC_TARGETS_EXTENSION.error_identifier
237        );
238    }
239
240    #[test]
241    fn resident_numeric_figure_target_rejects_without_provider_dispatch() {
242        let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
243            shape: vec![1, 1],
244            device_id: u32::MAX,
245            buffer_id: u64::MAX,
246            descriptor: Default::default(),
247        });
248        let err = futures::executor::block_on(close_builtin(vec![resident]))
249            .expect_err("resident figure target");
250        assert!(!err.message().to_ascii_lowercase().contains("provider"));
251    }
252
253    #[test]
254    fn invalid_network_structure_uses_canonical_invalid_handle_error() {
255        let invalid = Value::Struct(runmat_value::StructValue::new());
256        let err = futures::executor::block_on(close_builtin(vec![invalid]))
257            .expect_err("invalid networking handle");
258        assert_eq!(err.identifier(), Some("RunMat:close:InvalidHandle"));
259        assert!(CLOSE_DESCRIPTOR
260            .errors
261            .iter()
262            .any(|error| error.identifier == Some("RunMat:close:InvalidHandle")));
263    }
264}