Skip to main content

nifty_check/checker/
builtins.rs

1//! Calls to built-in functions.
2
3use std::rc::Rc;
4
5use nifty_syntax::Span;
6use nifty_syntax::ast::{self, Arg};
7
8use super::args::Args;
9use super::expr::reported;
10use super::{Checker, Global};
11use crate::elements::{BoundType, WINDOW};
12use crate::functions::{
13    CONNECTIONS, DEFAULT_CONNECTIONS, DEFAULT_RETRIES, DOWNLOAD, FORMAT_BYTES, FROM, Function,
14    FunctionKind, HEADERS, NAME, RETRIES, TITLE, TYPES, download_bound_type,
15};
16use crate::hir::{
17    Dialog, DialogKind, Download, DownloadCommand, DownloadField, Expr, ExprKind, Place, PlaceRoot,
18    PlaceStep, Type, WindowAction, WindowId,
19};
20
21impl Checker {
22    pub(super) fn built_in_call(
23        &mut self,
24        function: &'static Function,
25        args: &[Arg],
26        span: Span,
27    ) -> Expr {
28        let args = Args::built_in(self, function.name, function.params, args);
29        match function.kind {
30            FunctionKind::Download => self.download(args, span),
31            FunctionKind::PickFile => self.dialog(function.name, DialogKind::File, args, span),
32            FunctionKind::PickFiles => self.dialog(function.name, DialogKind::Files, args, span),
33            FunctionKind::PickFolder => self.dialog(function.name, DialogKind::Folder, args, span),
34            FunctionKind::PickSavePath => {
35                self.dialog(function.name, DialogKind::SavePath, args, span)
36            }
37            FunctionKind::OpenWindow => {
38                self.window_request(function.name, WindowAction::Open, args, span)
39            }
40            FunctionKind::CloseWindow => {
41                self.window_request(function.name, WindowAction::Close, args, span)
42            }
43            FunctionKind::FormatBytes => self.format_bytes(function, args, span),
44            FunctionKind::DownloadCommand(command) => self.download_command(command, args, span),
45        }
46    }
47
48    /// `open_window(settings)` or `close_window(settings)`. Opening and closing windows
49    /// changes the app, so the UI can't do it while it is shown.
50    fn window_request(
51        &mut self,
52        name: &str,
53        action: WindowAction,
54        mut args: Args,
55        span: Span,
56    ) -> Expr {
57        let window = if let Some(window) = args.positional() {
58            self.window_name(window)
59        } else {
60            self.error(
61                format!("`{name}` needs a window's name, such as `{name}(settings)`"),
62                span,
63            );
64            None
65        };
66        args.finish(self);
67        let Some(window) = window else {
68            return reported(span);
69        };
70        self.effects.writes = true;
71        Expr {
72            kind: ExprKind::Window(action, window),
73            ty: Type::Void,
74            span,
75        }
76    }
77
78    /// The window `expr` names.
79    fn window_name(&mut self, expr: &ast::Expr) -> Option<WindowId> {
80        let ast::ExprKind::Name(name) = &expr.kind else {
81            self.error("write a window's name here, such as `settings`", expr.span);
82            return None;
83        };
84        if let Some(Global::Window(window)) = self.global(name) {
85            return Some(window);
86        }
87        self.error(
88            format!(
89                "`{name}` isn't the name of a window; name one with `{WINDOW} {name}(\"Title\") {{ … }}`"
90            ),
91            expr.span,
92        );
93        None
94    }
95
96    /// `download(url, to: folder, progress: $percent, status: $text, …)`, with settings it
97    /// leaves out at their defaults. It changes the
98    /// bound state later, from another thread, so it counts as writing all of it now.
99    fn download(&mut self, mut args: Args, span: Span) -> Expr {
100        let url = self.required_arg(args.positional(), &Type::String, span, || {
101            format!(
102                "`{DOWNLOAD}` needs the address to fetch, such as `{DOWNLOAD}(\"https://…\", …)`"
103            )
104        });
105        let folder = self.required_arg(args.named("to"), &Type::String, span, || {
106            format!("`{DOWNLOAD}` needs a folder to save into, such as `to: \"~/Downloads\"`")
107        });
108        let name = self.optional_text(args.named(NAME), span);
109        let connections = self.optional_int(args.named(CONNECTIONS), DEFAULT_CONNECTIONS, span);
110        let headers = self.optional_texts(args.named(HEADERS), span);
111        let retries = self.optional_int(args.named(RETRIES), DEFAULT_RETRIES, span);
112        let mut bindings = Vec::new();
113        let mut all_bound = true;
114        for &field in DownloadField::ALL {
115            let arg = args.named(field.name());
116            if arg.is_none() && !field.is_required() {
117                continue;
118            }
119            let ty = download_bound_type(field).ty();
120            match self.bound_place(DOWNLOAD, Some(field.name()), arg, &ty, span) {
121                Some(place) => bindings.push((field, place)),
122                None => all_bound = false,
123            }
124        }
125        args.finish(self);
126        if !all_bound {
127            return reported(span);
128        }
129        let has_id = bindings
130            .iter()
131            .any(|(field, _)| *field == DownloadField::Id);
132        if !has_id
133            && bindings
134                .iter()
135                .any(|(_, place)| Self::is_in_list_item(place))
136        {
137            self.error(
138                format!(
139                    "`{DOWNLOAD}` reports into a list item here, so it needs `{id}: $…` in the same item, such as `{id}: $items[last].{id}`; its reports then stay with that item when the list changes",
140                    id = DownloadField::Id.name()
141                ),
142                span,
143            );
144            return reported(span);
145        }
146        for (_, place) in &bindings {
147            self.note_write(place);
148        }
149        Expr {
150            kind: ExprKind::Download(Box::new(Download {
151                url,
152                folder,
153                name,
154                connections,
155                headers,
156                retries,
157                bindings,
158            })),
159            ty: Type::Void,
160            span,
161        }
162    }
163
164    /// `cancel_download(id)` and the like. Asking changes what the download writes to
165    /// state, so it counts as writing.
166    fn download_command(&mut self, command: DownloadCommand, mut args: Args, span: Span) -> Expr {
167        let name = command.function();
168        let id = self.required_arg(args.positional(), &Type::String, span, || {
169            format!("`{name}` needs a download's id, such as `{name}(item.id)`")
170        });
171        args.finish(self);
172        self.effects.writes = true;
173        Expr {
174            kind: ExprKind::DownloadCommand(command, Box::new(id)),
175            ty: Type::Void,
176            span,
177        }
178    }
179
180    /// `format_bytes(n)`, which only reads, so the UI may show it.
181    fn format_bytes(&mut self, function: &Function, mut args: Args, span: Span) -> Expr {
182        let bytes = self.required_arg(args.positional(), &Type::Int, span, || {
183            format!("`{FORMAT_BYTES}` needs a number of bytes, such as `{FORMAT_BYTES}(1500)`")
184        });
185        args.finish(self);
186        Expr {
187            kind: ExprKind::FormatBytes(Box::new(bytes)),
188            ty: function.value.map_or(Type::Void, BoundType::ty),
189            span,
190        }
191    }
192
193    /// `pick_file($path, title: …, types: […], from: …)` and its siblings. The answer is
194    /// written later, once the user closes the dialog, so it counts as writing it now.
195    fn dialog(&mut self, name: &str, kind: DialogKind, mut args: Args, span: Span) -> Expr {
196        let answer_type = if kind.picks_many() {
197            BoundType::Strings
198        } else {
199            BoundType::String
200        };
201        let answer = self.bound_place(name, None, args.positional(), &answer_type.ty(), span);
202        let title = self.optional_text(args.named(TITLE), span);
203        let types = match kind {
204            DialogKind::Folder => no_texts(span),
205            DialogKind::File | DialogKind::Files | DialogKind::SavePath => {
206                self.optional_texts(args.named(TYPES), span)
207            }
208        };
209        let file_name = match kind {
210            DialogKind::SavePath => self.optional_text(args.named(NAME), span),
211            DialogKind::File | DialogKind::Files | DialogKind::Folder => empty_text(span),
212        };
213        let folder = self.optional_text(args.named(FROM), span);
214        args.finish(self);
215        let Some(answer) = answer else {
216            return reported(span);
217        };
218        self.note_write(&answer);
219        Expr {
220            kind: ExprKind::Dialog(Box::new(Dialog {
221                kind,
222                answer,
223                title,
224                types,
225                folder,
226                name: file_name,
227            })),
228            ty: Type::Void,
229            span,
230        }
231    }
232
233    /// Whether the bound `place` is inside a list's item, whose position can change. A
234    /// bound place rooted in a local is a `for` variable over a list's items.
235    fn is_in_list_item(place: &Place) -> bool {
236        matches!(place.root, PlaceRoot::Local(_))
237            || place
238                .path
239                .iter()
240                .any(|step| matches!(step, PlaceStep::Index(_)))
241    }
242
243    fn optional_text(&mut self, arg: Option<&ast::Expr>, span: Span) -> Expr {
244        arg.map_or_else(|| empty_text(span), |arg| self.expect(arg, &Type::String))
245    }
246
247    /// A `[string]` setting, empty when left out.
248    fn optional_texts(&mut self, arg: Option<&ast::Expr>, span: Span) -> Expr {
249        let strings = BoundType::Strings.ty();
250        arg.map_or_else(|| no_texts(span), |arg| self.expect(arg, &strings))
251    }
252
253    fn optional_int(&mut self, arg: Option<&ast::Expr>, default: i64, span: Span) -> Expr {
254        arg.map_or_else(
255            || Expr {
256                kind: ExprKind::Int(default),
257                ty: Type::Int,
258                span,
259            },
260            |arg| self.expect(arg, &Type::Int),
261        )
262    }
263}
264
265fn empty_text(span: Span) -> Expr {
266    Expr {
267        kind: ExprKind::String(Rc::from("")),
268        ty: Type::String,
269        span,
270    }
271}
272
273fn no_texts(span: Span) -> Expr {
274    Expr {
275        kind: ExprKind::List(Vec::new()),
276        ty: BoundType::Strings.ty(),
277        span,
278    }
279}