doge_compiler/ast/mod.rs
1use crate::token::Span;
2
3mod analysis;
4mod dump;
5#[cfg(test)]
6mod tests;
7
8pub use analysis::{celled_locals, child_funcdefs, free_names};
9pub use dump::dump;
10
11/// A whole parsed script: a sequence of top-level statements (the terminating
12/// `wow` is consumed by the parser and not stored).
13#[derive(Debug, Clone, PartialEq)]
14pub struct Script {
15 pub stmts: Vec<Stmt>,
16}
17
18/// One formal parameter in a function header: its name and, optionally, a
19/// default value. A default is a literal (docs/SYNTAX.md §6), so it references no
20/// names and is safe to evaluate fresh at every call site.
21#[derive(Debug, Clone, PartialEq)]
22pub struct Param {
23 pub name: String,
24 pub default: Option<Expr>,
25 pub span: Span,
26}
27
28/// A function header's parameters: the fixed/defaulted positional parameters in
29/// order, plus an optional trailing variadic parameter (`many rest`) that
30/// collects the surplus arguments into a List. Required parameters come first,
31/// then defaulted ones, then the variadic — the parser enforces that order.
32#[derive(Debug, Clone, PartialEq, Default)]
33pub struct Params {
34 pub params: Vec<Param>,
35 pub vararg: Option<String>,
36}
37
38impl Params {
39 /// True when the header declares no parameters at all.
40 pub fn is_empty(&self) -> bool {
41 self.params.is_empty() && self.vararg.is_none()
42 }
43
44 /// Whether a trailing `many rest` variadic parameter is present.
45 pub fn has_vararg(&self) -> bool {
46 self.vararg.is_some()
47 }
48
49 /// The number of leading parameters with no default — the minimum a call must
50 /// supply.
51 pub fn required(&self) -> usize {
52 self.params.iter().filter(|p| p.default.is_none()).count()
53 }
54
55 /// The largest number of positional arguments the header can bind without a
56 /// variadic parameter: every named parameter. `None` when a variadic is
57 /// present (unbounded).
58 pub fn max_positional(&self) -> Option<usize> {
59 if self.has_vararg() {
60 None
61 } else {
62 Some(self.params.len())
63 }
64 }
65
66 /// The names the body binds: every parameter, then the variadic (which
67 /// arrives as an already-packed List). This is what the compiled wrapper
68 /// takes as value parameters.
69 pub fn binding_names(&self) -> Vec<String> {
70 let mut names: Vec<String> = self.params.iter().map(|p| p.name.clone()).collect();
71 if let Some(rest) = &self.vararg {
72 names.push(rest.clone());
73 }
74 names
75 }
76
77 /// The call shape shown in an arity hint, e.g. `greet(name, mood = …, many rest)`.
78 pub fn render(&self, callee: &str) -> String {
79 let mut parts: Vec<String> = self
80 .params
81 .iter()
82 .map(|p| {
83 if p.default.is_some() {
84 format!("{} = …", p.name)
85 } else {
86 p.name.clone()
87 }
88 })
89 .collect();
90 if let Some(rest) = &self.vararg {
91 parts.push(format!("many {rest}"));
92 }
93 format!("{callee}({})", parts.join(", "))
94 }
95}
96
97/// One statement. Variants mirror the docs/GRAMMAR.md grammar.
98// `ExprStmt` is the conventional AST name for a bare-expression statement; the
99// suffix intentionally echoes the enum name, so silence that one style lint.
100#[allow(clippy::enum_variant_names)]
101#[derive(Debug, Clone, PartialEq)]
102pub enum Stmt {
103 /// `such x = e`, or a destructuring `such a, b = e` / `such a, many rest = e`.
104 /// `names` are the leading targets in order; `rest`, when present, is a
105 /// trailing `many` collector that gathers the surplus into a List. A plain
106 /// single declaration has one name and no `rest`.
107 Decl {
108 names: Vec<String>,
109 rest: Option<String>,
110 expr: Expr,
111 span: Span,
112 },
113 /// `so X = e`
114 ConstDecl {
115 name: String,
116 expr: Expr,
117 span: Span,
118 },
119 /// `so math`, or the string-path form `so "lib/utils.doge"`. `module` is the
120 /// binding name (the stem for a path import); `path` is the raw string of a
121 /// path import, `None` for a bare-name import.
122 Import {
123 module: String,
124 path: Option<String>,
125 span: Span,
126 },
127 /// `[very] target = e`, or a destructuring `[very] a, b = e` /
128 /// `[very] a, many rest = e` — `flavored` is true when written with `very`.
129 /// `targets` are the leading assignable targets in order; `rest`, when
130 /// present, is a trailing `many` collector target that gathers the surplus
131 /// into a List. `op` is `Some` for an augmented assignment (`target op= e`),
132 /// which reads the target, applies the binary operator, and stores the result
133 /// back — augmented assignment is single-target only (one `targets` entry, no
134 /// `rest`).
135 Assign {
136 targets: Vec<Expr>,
137 rest: Option<Expr>,
138 expr: Expr,
139 op: Option<BinOp>,
140 flavored: bool,
141 span: Span,
142 },
143 /// `bark e`
144 Bark { expr: Expr, span: Span },
145 /// `if / elif / else`. Each branch is (condition, body); `else_body` is the
146 /// optional trailing `else` block.
147 If {
148 branches: Vec<(Expr, Vec<Stmt>)>,
149 else_body: Option<Vec<Stmt>>,
150 span: Span,
151 },
152 /// `for v in iter:`, or a destructuring `for k, v in iter:` /
153 /// `for first, many rest in iter:`. `vars` are the leading loop variables in
154 /// order; `rest`, when present, is a trailing `many` collector that gathers
155 /// each element's surplus into a List. A plain loop has one var and no `rest`.
156 For {
157 vars: Vec<String>,
158 rest: Option<String>,
159 iter: Expr,
160 body: Vec<Stmt>,
161 span: Span,
162 },
163 /// `while cond:`
164 While {
165 cond: Expr,
166 body: Vec<Stmt>,
167 span: Span,
168 },
169 /// `such name much params: … wow`
170 FuncDef {
171 name: String,
172 params: Params,
173 body: Vec<Stmt>,
174 span: Span,
175 },
176 /// `many Name [much Parent]: … wow` — a body of function definitions
177 /// (methods). `parent` names the class it inherits from, when one is given.
178 ObjDef {
179 name: String,
180 parent: Option<String>,
181 methods: Vec<Stmt>,
182 span: Span,
183 },
184 /// `pls … oh no err! …`
185 Try {
186 body: Vec<Stmt>,
187 err_name: String,
188 handler: Vec<Stmt>,
189 span: Span,
190 },
191 /// `return [e]`
192 Return { expr: Option<Expr>, span: Span },
193 /// `bonk e` — raise a catchable error whose message is `e`'s display form.
194 Bonk { expr: Expr, span: Span },
195 /// `amaze cond [, message]` — assert: a no-op when `cond` is truthy, else a
196 /// catchable `AssertError` whose message is `message`'s display form (or a
197 /// default when omitted). The message is evaluated only on failure.
198 Amaze {
199 cond: Expr,
200 message: Option<Expr>,
201 span: Span,
202 },
203 /// `bork`
204 Bork { span: Span },
205 /// `continue`
206 Continue { span: Span },
207 /// A bare expression used as a statement, e.g. a call `greet(x)`.
208 ExprStmt { expr: Expr },
209}
210
211/// One expression.
212#[derive(Debug, Clone, PartialEq)]
213pub enum Expr {
214 Int {
215 value: i64,
216 span: Span,
217 },
218 Float {
219 value: f64,
220 span: Span,
221 },
222 Str {
223 value: String,
224 span: Span,
225 },
226 Bool {
227 value: bool,
228 span: Span,
229 },
230 None {
231 span: Span,
232 },
233 Ident {
234 name: String,
235 span: Span,
236 },
237 List {
238 items: Vec<Expr>,
239 span: Span,
240 },
241 Dict {
242 entries: Vec<(Expr, Expr)>,
243 span: Span,
244 },
245 Binary {
246 op: BinOp,
247 lhs: Box<Expr>,
248 rhs: Box<Expr>,
249 span: Span,
250 },
251 Unary {
252 op: UnOp,
253 operand: Box<Expr>,
254 span: Span,
255 },
256 /// `f(a, b, key = c)` — positional `args`, then any keyword arguments as
257 /// `(name, value)` pairs in source order. Keyword arguments are only accepted
258 /// where the callee is known at compile time (docs/SYNTAX.md §6).
259 Call {
260 callee: Box<Expr>,
261 args: Vec<Expr>,
262 kwargs: Vec<(String, Expr)>,
263 span: Span,
264 },
265 Index {
266 obj: Box<Expr>,
267 index: Box<Expr>,
268 span: Span,
269 },
270 /// `obj[start:end:step]` — each bound is optional (`None` means the default
271 /// end of that side), matching Python slice semantics.
272 Slice {
273 obj: Box<Expr>,
274 start: Option<Box<Expr>>,
275 end: Option<Box<Expr>>,
276 step: Option<Box<Expr>>,
277 span: Span,
278 },
279 /// `then if cond else otherwise` — only the taken branch is evaluated.
280 Ternary {
281 cond: Box<Expr>,
282 then: Box<Expr>,
283 otherwise: Box<Expr>,
284 span: Span,
285 },
286 Attr {
287 obj: Box<Expr>,
288 name: String,
289 span: Span,
290 },
291 /// `super.method(args)` — call a method inherited from the enclosing class's
292 /// parent, resolved statically. Only valid inside a method of a class that has
293 /// a parent (docs/SYNTAX.md §8).
294 SuperCall {
295 method: String,
296 args: Vec<Expr>,
297 span: Span,
298 },
299 StrInterp {
300 parts: Vec<InterpPart>,
301 span: Span,
302 },
303}
304
305/// One piece of a string-interpolation expression (`"a {b} c"`): literal text or
306/// an embedded expression whose display form is spliced in at runtime.
307#[derive(Debug, Clone, PartialEq)]
308pub enum InterpPart {
309 Lit(String),
310 Expr(Expr),
311}
312
313/// Binary operators, in the spelling they print with in the dump.
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum BinOp {
316 Add,
317 Sub,
318 Mul,
319 Div,
320 FloorDiv,
321 Rem,
322 Pow,
323 BitAnd,
324 BitOr,
325 BitXor,
326 Shl,
327 Shr,
328 Eq,
329 NotEq,
330 Lt,
331 LtEq,
332 Gt,
333 GtEq,
334 In,
335 NotIn,
336 And,
337 Or,
338}
339
340impl BinOp {
341 pub fn symbol(self) -> &'static str {
342 match self {
343 BinOp::Add => "+",
344 BinOp::Sub => "-",
345 BinOp::Mul => "*",
346 BinOp::Div => "/",
347 BinOp::FloorDiv => "//",
348 BinOp::Rem => "%",
349 BinOp::Pow => "**",
350 BinOp::BitAnd => "&",
351 BinOp::BitOr => "|",
352 BinOp::BitXor => "^",
353 BinOp::Shl => "<<",
354 BinOp::Shr => ">>",
355 BinOp::Eq => "==",
356 BinOp::NotEq => "!=",
357 BinOp::Lt => "<",
358 BinOp::LtEq => "<=",
359 BinOp::Gt => ">",
360 BinOp::GtEq => ">=",
361 BinOp::In => "in",
362 BinOp::NotIn => "not in",
363 BinOp::And => "and",
364 BinOp::Or => "or",
365 }
366 }
367}
368
369/// Unary operators: `not` (logical) and `neg` (numeric minus).
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub enum UnOp {
372 Not,
373 Neg,
374 BitNot,
375}
376
377impl UnOp {
378 pub fn symbol(self) -> &'static str {
379 match self {
380 UnOp::Not => "not",
381 UnOp::Neg => "neg",
382 UnOp::BitNot => "~",
383 }
384 }
385}
386
387impl Stmt {
388 /// The source span this statement starts at.
389 pub fn span(&self) -> Span {
390 match self {
391 Stmt::Decl { span, .. }
392 | Stmt::ConstDecl { span, .. }
393 | Stmt::Import { span, .. }
394 | Stmt::Assign { span, .. }
395 | Stmt::Bark { span, .. }
396 | Stmt::If { span, .. }
397 | Stmt::For { span, .. }
398 | Stmt::While { span, .. }
399 | Stmt::FuncDef { span, .. }
400 | Stmt::ObjDef { span, .. }
401 | Stmt::Try { span, .. }
402 | Stmt::Return { span, .. }
403 | Stmt::Bonk { span, .. }
404 | Stmt::Amaze { span, .. }
405 | Stmt::Bork { span }
406 | Stmt::Continue { span } => *span,
407 Stmt::ExprStmt { expr } => expr.span(),
408 }
409 }
410}
411
412impl Expr {
413 /// The source span this expression starts at.
414 pub fn span(&self) -> Span {
415 match self {
416 Expr::Int { span, .. }
417 | Expr::Float { span, .. }
418 | Expr::Str { span, .. }
419 | Expr::Bool { span, .. }
420 | Expr::None { span }
421 | Expr::Ident { span, .. }
422 | Expr::List { span, .. }
423 | Expr::Dict { span, .. }
424 | Expr::Binary { span, .. }
425 | Expr::Unary { span, .. }
426 | Expr::Call { span, .. }
427 | Expr::Index { span, .. }
428 | Expr::Slice { span, .. }
429 | Expr::Ternary { span, .. }
430 | Expr::Attr { span, .. }
431 | Expr::SuperCall { span, .. }
432 | Expr::StrInterp { span, .. } => *span,
433 }
434 }
435}
436
437/// Call `f` with each statement block a statement contributes to its *enclosing*
438/// scope: an `if`'s branch and `else` bodies, a `for`/`while` body, and a
439/// `pls`/`oh no` body and handler. A nested function's or object's own body is
440/// deliberately not visited — those statements belong to their own scope, not
441/// this one. This match is exhaustive with no wildcard, so a new block-carrying
442/// statement cannot be silently skipped by the scope collectors and capture
443/// analysis that fold over it.
444pub fn for_each_child_block<'a>(stmt: &'a Stmt, f: &mut impl FnMut(&'a [Stmt])) {
445 match stmt {
446 Stmt::If {
447 branches,
448 else_body,
449 ..
450 } => {
451 for (_, body) in branches {
452 f(body);
453 }
454 if let Some(body) = else_body {
455 f(body);
456 }
457 }
458 Stmt::For { body, .. } | Stmt::While { body, .. } => f(body),
459 Stmt::Try { body, handler, .. } => {
460 f(body);
461 f(handler);
462 }
463 Stmt::Decl { .. }
464 | Stmt::ConstDecl { .. }
465 | Stmt::Import { .. }
466 | Stmt::Assign { .. }
467 | Stmt::Bark { .. }
468 | Stmt::FuncDef { .. }
469 | Stmt::ObjDef { .. }
470 | Stmt::Return { .. }
471 | Stmt::Bonk { .. }
472 | Stmt::Amaze { .. }
473 | Stmt::Bork { .. }
474 | Stmt::Continue { .. }
475 | Stmt::ExprStmt { .. } => {}
476 }
477}
478
479/// Every bound name in one scope — `such`/`so` declarations, `for` loop
480/// variables, `oh no` error names, and nested function definitions — in
481/// first-seen order, each once. These become the scope's `Env` fields or hoisted
482/// locals. A nested function's own body is not descended into: its inner names
483/// belong to its own scope.
484pub fn hoisted_names(stmts: &[Stmt]) -> Vec<String> {
485 let mut names = Vec::new();
486 collect_hoisted(stmts, &mut names);
487 names
488}
489
490/// The top-level hoisted names for `Env` fields: like [`hoisted_names`] but a
491/// direct top-level `such name:` / `many Name:` is a static definition, not an
492/// `Env` field, so those direct definitions are skipped (a function nested inside
493/// a top-level `if`/`for` block is still a closure and does get a field).
494pub(crate) fn toplevel_hoisted(stmts: &[Stmt]) -> Vec<String> {
495 let mut names = Vec::new();
496 for stmt in stmts {
497 if matches!(stmt, Stmt::FuncDef { .. } | Stmt::ObjDef { .. }) {
498 continue;
499 }
500 collect_hoisted(std::slice::from_ref(stmt), &mut names);
501 }
502 names
503}
504
505pub(crate) fn collect_hoisted(stmts: &[Stmt], names: &mut Vec<String>) {
506 for stmt in stmts {
507 match stmt {
508 // A nested function binds its name in this scope; its body is its own.
509 Stmt::ConstDecl { name, .. } | Stmt::FuncDef { name, .. } => push_unique(names, name),
510 Stmt::Decl {
511 names: decl_names,
512 rest,
513 ..
514 } => {
515 for name in decl_names {
516 push_unique(names, name);
517 }
518 if let Some(rest) = rest {
519 push_unique(names, rest);
520 }
521 }
522 Stmt::For { vars, rest, .. } => {
523 for var in vars {
524 push_unique(names, var);
525 }
526 if let Some(rest) = rest {
527 push_unique(names, rest);
528 }
529 }
530 Stmt::Try { err_name, .. } => push_unique(names, err_name),
531 _ => {}
532 }
533 for_each_child_block(stmt, &mut |body| collect_hoisted(body, names));
534 }
535}
536
537pub(crate) fn push_unique(names: &mut Vec<String>, name: &str) {
538 if !names.iter().any(|n| n == name) {
539 names.push(name.to_string());
540 }
541}