1use 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 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 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 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 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 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 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 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 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}