--- /dev/null
+use std::collections::HashMap;
+
+use unicase::UniCase;
+
+use crate::{
+ lex::token::{MacroToken, Token},
+ message::Location,
+};
+
+/// A PSPP macro as defined with `!DEFINE`.
+pub struct Macro {
+ /// The macro's name. This is an ordinary identifier except that it is
+ /// allowed (but not required) to begin with `!`.
+ pub name: String,
+
+ /// Source code location of macro definition, for error reporting.
+ pub location: Location,
+
+ /// Parameters.
+ parameters: Vec<Parameter>,
+
+ /// Body.
+ body: Vec<BodyToken>,
+}
+
+struct Parameter {
+ /// `!name` or `!1`.
+ name: String,
+
+ /// Default value.
+ ///
+ /// The tokens don't include white space, etc. between them.
+ default: Vec<BodyToken>,
+
+ /// Macro-expand the argument?
+ expand_arg: bool,
+
+ /// How the argument is specified.
+ arg: Arg,
+}
+
+impl Parameter {
+ /// Returns true if this is a positional parameter. Positional parameters
+ /// are expanded by index (position) rather than by name.
+ fn is_positional(&self) -> bool {
+ self.name.as_bytes()[1].is_ascii_digit()
+ }
+}
+
+enum Arg {
+ /// Argument consists of `.0` tokens.
+ NTokens(usize),
+
+ /// Argument runs until token `.0`.
+ CharEnd(Token),
+
+ /// Argument starts with token `.0` and ends with token `.1`.
+ Enclose(Token, Token),
+
+ /// Argument runs until the end of the command.
+ CmdEnd,
+}
+
+/// A token and the syntax that was tokenized to produce it. The syntax allows
+/// the token to be turned back into syntax accurately.
+struct BodyToken {
+ /// The token.
+ token: Token,
+
+ /// The syntax that produces `token`.
+ syntax: String,
+}
+
+type MacroSet = HashMap<UniCase<String>, Macro>;
+
+pub enum MacroCallState {
+ Arg,
+ Enclose,
+ Keyword,
+ Equals,
+ Finished,
+}
+
+pub struct MacroCallBuilder<'a> {
+ macros: &'a MacroSet,
+}
+
+impl<'a> MacroCallBuilder<'a> {
+ fn new(macro_set: &'a MacroSet, token: &Token) -> Option<Self> {
+ let macro_name = match token {
+ Token::Id(s) => s,
+ Token::MacroToken(MacroToken::MacroId(s)) => s,
+ _ => return None,
+ }.clone();
+ let Some(macro_) = macro_set.get(&UniCase::new(macro_name)) else {
+ return None;
+ };
+ }
+}
--- /dev/null
+use std::{
+ cmp::{max, min},
+ fmt::Result as FmtResult,
+ fmt::{Display, Formatter},
+ ops::RangeInclusive,
+ sync::Arc,
+};
+
+use unicode_width::UnicodeWidthStr;
+
+/// A line number and optional column number within a source file.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Point {
+ /// 1-based line number.
+ line: i32,
+
+ /// 1-based column number.
+ ///
+ /// Column numbers are measured according to the width of characters as shown in
+ /// a typical fixed-width font, in which CJK characters have width 2 and
+ /// combining characters have width 0.
+ column: Option<i32>,
+}
+
+impl Point {
+ /// Takes `point`, adds to it the syntax in `syntax`, incrementing the line
+ /// number for each new-line in `syntax` and the column number for each
+ /// column, and returns the result.
+ pub fn advance(&self, syntax: &str) -> Self {
+ let mut result = *self;
+ for line in syntax.split_inclusive('\n') {
+ if line.ends_with('\n') {
+ result.line += 1;
+ result.column = Some(1);
+ } else {
+ result.column = result.column.map(|column| column + line.width() as i32);
+ }
+ }
+ result
+ }
+
+ pub fn without_column(&self) -> Self {
+ Self {
+ line: self.line,
+ column: None,
+ }
+ }
+}
+
+/// Location relevant to an diagnostic message.
+#[derive(Clone, Debug)]
+pub struct Location {
+ /// File name, if any.
+ pub file_name: Option<Arc<String>>,
+
+ /// Starting and ending point, if any.
+ pub span: Option<RangeInclusive<Point>>,
+
+ /// Normally, if `span` contains column information, then displaying the
+ /// message will underline the location. Setting this to true disables
+ /// displaying underlines.
+ pub omit_underlines: bool,
+}
+
+impl Display for Location {
+ fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+ if let Some(file_name) = &self.file_name {
+ write!(f, "{}", file_name)?;
+ }
+
+ if let Some(span) = &self.span {
+ if self.file_name.is_some() {
+ write!(f, ":")?;
+ }
+ let l1 = span.start().line;
+ let l2 = span.end().line;
+ if let (Some(c1), Some(c2)) = (span.start().column, span.end().column) {
+ if l2 > l1 {
+ write!(f, "{l1}.{c1}-{l2}.{c2}")?;
+ } else {
+ write!(f, "{l1}.{c1}-{c2}")?;
+ }
+ } else {
+ if l2 > l1 {
+ write!(f, "{l1}-{l2}")?;
+ } else {
+ write!(f, "{l1}")?;
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+impl Location {
+ pub fn without_columns(&self) -> Self {
+ Self {
+ file_name: self.file_name.clone(),
+ span: self
+ .span
+ .as_ref()
+ .map(|span| span.start().without_column()..=span.end().without_column()),
+ omit_underlines: self.omit_underlines,
+ }
+ }
+ pub fn merge(a: Option<Self>, b: &Option<Self>) -> Option<Self> {
+ let Some(a) = a else { return b.clone() };
+ let Some(b) = b else { return Some(a) };
+ if a.file_name != b.file_name {
+ // Failure.
+ return Some(a);
+ }
+ let span = match (&a.span, &b.span) {
+ (None, None) => None,
+ (Some(r), None) | (None, Some(r)) => Some(r.clone()),
+ (Some(ar), Some(br)) => {
+ Some(min(ar.start(), br.start()).clone()..=max(ar.end(), br.end()).clone())
+ }
+ };
+ Some(Self {
+ file_name: a.file_name,
+ span,
+ omit_underlines: a.omit_underlines || b.omit_underlines,
+ })
+ }
+ pub fn is_empty(&self) -> bool {
+ self.file_name.is_none() && self.span.is_none()
+ }
+}