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