Skip to main content

ironwork_compile/
function.rs

1//! User-defined functions (FUNCTION-ID): the rules a definition or prototype keeps, the
2//! prototypes an invocation is checked against (Language Reference, the USING phrase's conformance
3//! of parameters for user-defined functions), and the formal parameters and result the
4//! interpreter invokes one with.
5
6use crate::corresponding::is_alphabetic;
7use crate::layout::{self, Kind, Layout, Resolved};
8use crate::picture::Notation;
9use numeric::Qualify;
10use rt::picture::Sym;
11use syntax::ast::*;
12use syntax::{Error, Pos};
13
14/// A user-defined function as an invocation sees it.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Udf {
17    pub name: String,
18    pub external: String,
19    pub params: Vec<Formal>,
20    pub result: Formal,
21    pub pos: Pos,
22}
23
24/// A formal parameter or the RETURNING item: what an argument conforms to, and the shape of the
25/// temporary that holds a literal or expression argument.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct Formal {
28    pub name: String,
29    pub by_value: bool,
30    pub kind: Kind,
31    pub size: u32,
32    pub scaling: u32,
33    pub alphabetic: bool,
34    /// An edited item's PICTURE symbols and the currency string its currency symbol stands for.
35    pub edit: Option<(Vec<Sym>, String)>,
36    pub decimal_point_comma: bool,
37}
38
39impl Udf {
40    /// Whether the function is alphanumeric or national, so its value can be reference-modified.
41    pub fn character_valued(&self) -> bool {
42        matches!(self.result.kind, Kind::Group | Kind::Alnum { .. } | Kind::AlnumEdited { .. } | Kind::National)
43    }
44}
45
46/// The function as its prototype's LINKAGE SECTION and PROCEDURE DIVISION header describe it.
47pub fn signature(p: &Prototype, qualify: Qualify) -> Result<Udf, Error> {
48    let layout = layout::build(&[], &[], &[], &p.linkage, &[], Notation::of(&p.environment), qualify, None)?;
49    let alphabetic: Vec<Pos> = p.linkage.iter().filter(|e| e.picture.as_deref().is_some_and(is_alphabetic)).map(|e| e.pos).collect();
50    let formal = |name: &str, by_value: bool| {
51        let root = layout.linkage_roots.iter().copied().find(|&i| layout.items[i].name.as_deref() == Some(name));
52        let item = &layout.items[root.ok_or_else(|| Error::at(p.pos, format!("FUNCTION-ID {}: {name} is not an 01 or 77 item of the LINKAGE SECTION", p.name)))?];
53        let edit = match item.kind {
54            Kind::NumericEdited { edit, .. } | Kind::AlnumEdited { edit } => Some((layout.edits[edit as usize].clone(), layout.currencies[edit as usize].clone())),
55            _ => None,
56        };
57        Ok(Formal {
58            name: name.to_owned(),
59            by_value,
60            kind: item.kind,
61            size: item.size,
62            scaling: item.scaling,
63            alphabetic: alphabetic.contains(&item.pos),
64            edit,
65            decimal_point_comma: p.environment.decimal_point_comma,
66        })
67    };
68    let returning = p.returning.as_deref().ok_or_else(|| Error::at(p.pos, format!("FUNCTION-ID {}: a user-defined function needs PROCEDURE DIVISION RETURNING", p.name)))?;
69    let params = p.using.iter().map(|u| formal(&u.name, u.by_value)).collect::<Result<_, _>>()?;
70    Ok(Udf { name: p.name.clone(), external: p.external.clone(), params, result: formal(returning, false)?, pos: p.pos })
71}
72
73/// The functions a program may invoke, each laid out once. A prototype that does not lay out is
74/// left out here: compiling it reports why, and an invocation of it is then unknown.
75pub fn functions(program: &Program, qualify: Qualify, errors: &mut Vec<Error>) -> Vec<Udf> {
76    let own = program.function.as_ref().map(|f| f.pos);
77    let mut out = Vec::new();
78    for p in &program.prototypes {
79        match signature(p, qualify) {
80            Ok(udf) => out.push(udf),
81            Err(e) if Some(p.pos) == own => errors.push(e),
82            Err(_) => {}
83        }
84    }
85    if let Some(own) = own {
86        definition_rules(program, &out, own, errors);
87    }
88    facilities(program, errors);
89    out
90}
91
92/// SQL and CICS cannot be used with user-defined functions (Programming Guide, Structuring
93/// user-defined functions): not in a function, nor in a program its source defines or prototypes
94/// one before.
95fn facilities(program: &Program, errors: &mut Vec<Error>) {
96    if program.function.is_none() && program.prototypes.is_empty() {
97        return;
98    }
99    let mut all = Vec::new();
100    program.paragraphs.iter().for_each(|p| crate::inner_statements(&p.statements, &mut all));
101    let statements = all.into_iter().filter_map(|s| match s {
102        Stmt::Exec(b) => Some(b.as_ref()),
103        _ => None,
104    });
105    if let Some(b) = program.exec_declarations.iter().chain(statements).find(|b| matches!(b.kind, ExecKind::Sql | ExecKind::Cics)) {
106        let kind = if b.kind == ExecKind::Sql { "SQL" } else { "CICS" };
107        errors.push(Error::at(b.pos, format!("EXEC {kind}: SQL and CICS cannot be used with user-defined functions, so neither in one nor in a program after one in its source (assumption C273)")));
108    }
109}
110
111/// What a function definition or prototype keeps of the rules: BY VALUE parameters of the kinds
112/// that can be passed by value, and agreement with every prototype of its name before it.
113fn definition_rules(program: &Program, functions: &[Udf], own: Pos, errors: &mut Vec<Error>) {
114    let Some(this) = functions.iter().find(|u| u.pos == own) else { return };
115    for f in this.params.iter().filter(|f| f.by_value) {
116        let one_character = matches!(f.kind, Kind::Alnum { .. }) && f.size == 1 || f.kind == Kind::National && f.size == 2;
117        if !(one_character || matches!(f.kind, Kind::Binary { .. } | Kind::Float(_) | Kind::Pointer | Kind::ProgramPointer)) {
118            errors.push(Error::at(own, format!("PROCEDURE DIVISION USING BY VALUE {}: a function's BY VALUE parameter is binary, floating-point, a pointer, or one alphanumeric or national character", f.name)));
119        }
120    }
121    for other in functions.iter().filter(|u| u.name == this.name && u.pos != own) {
122        if let Some(why) = disagreement(this, other) {
123            errors.push(Error::at(own, format!("FUNCTION-ID {}: {why} from the prototype at line {}", program.id, other.pos.line)));
124        }
125    }
126}
127
128fn disagreement(a: &Udf, b: &Udf) -> Option<String> {
129    if a.external != b.external {
130        return Some(format!("the external name {} differs", a.external));
131    }
132    if a.params.len() != b.params.len() {
133        return Some(format!("{} parameters differ in number", a.params.len()));
134    }
135    let same = |x: &Formal, y: &Formal| (x.by_value, x.kind, x.size, x.scaling, x.alphabetic, &x.edit) == (y.by_value, y.kind, y.size, y.scaling, y.alphabetic, &y.edit);
136    if let Some(k) = (0..a.params.len()).find(|&k| !same(&a.params[k], &b.params[k])) {
137        return Some(format!("parameter {} ({}) differs", k + 1, a.params[k].name));
138    }
139    (!same(&a.result, &b.result)).then(|| format!("the RETURNING item {} differs", a.result.name))
140}
141
142/// Why argument `item` of the caller's `layout` does not conform to `formal`. A literal or an
143/// expression has no description to conform: it is moved or computed into a temporary of the
144/// formal parameter's (assumption C272).
145pub fn conformance(layout: &Layout, item: usize, alphabetic: bool, decimal_point_comma: bool, formal: &Formal) -> Option<String> {
146    let it = &layout.items[item];
147    if formal.by_value {
148        if it.kind == Kind::Group {
149            return Some("a group is never passed BY VALUE".into());
150        }
151        if arithmetic(formal.kind) && !arithmetic(it.kind) {
152            return Some(format!("BY VALUE {} is numeric, and takes an argument COMPUTE could send it", formal.name));
153        }
154        return None;
155    }
156    if it.kind == Kind::Group || formal.kind == Kind::Group {
157        return (it.size < formal.size).then(|| format!("{} bytes cannot be passed BY REFERENCE to the {}-byte {}", it.size, formal.size, formal.name));
158    }
159    let edit = match it.kind {
160        Kind::NumericEdited { edit, .. } | Kind::AlnumEdited { edit } => Some((layout.edits[edit as usize].clone(), layout.currencies[edit as usize].clone())),
161        _ => None,
162    };
163    let conforms = match (it.kind, formal.kind) {
164        (Kind::Alnum { justified: a }, Kind::Alnum { justified: b }) => a == b && alphabetic == formal.alphabetic && it.size >= formal.size,
165        (Kind::National, Kind::National) => it.size >= formal.size,
166        (a, b) => a == b && it.size == formal.size && it.scaling == formal.scaling && edit == formal.edit && (edit.is_none() || decimal_point_comma == formal.decimal_point_comma),
167    };
168    (!conforms).then(|| format!("{} is passed BY REFERENCE, so its PICTURE, USAGE, SIGN, JUSTIFIED and BLANK WHEN ZERO must be the argument's", formal.name))
169}
170
171/// The invocation of `udf` at `f`: its argument count, and each data-item argument against its
172/// formal parameter.
173pub fn check_invocation(udf: &Udf, f: &FunctionCall, layout: &Layout, alphabetic: &[Pos], decimal_point_comma: bool, errors: &mut Vec<Error>) {
174    let name = &f.name;
175    if f.args.len() != udf.params.len() {
176        errors.push(Error::at(f.pos, format!("FUNCTION {name} takes {} arguments, not {}", udf.params.len(), f.args.len())));
177        return;
178    }
179    if f.modifier.is_some() || !f.all_subscripts.is_empty() {
180        errors.push(Error::at(f.pos, format!("FUNCTION {name}: a user-defined function's argument is an identifier, a literal or an arithmetic expression")));
181    }
182    if f.refmod.is_some() && !udf.character_valued() {
183        errors.push(Error::at(f.pos, format!("FUNCTION {name}: only an alphanumeric or national function's value can be reference-modified")));
184    }
185    for (k, (arg, formal)) in f.args.iter().zip(&udf.params).enumerate() {
186        let r = match arg {
187            Expr::Operand(Operand::Literal(Literal::Figurative(_) | Literal::All(_))) => {
188                errors.push(Error::at(f.pos, format!("FUNCTION {name} argument {}: a function's argument is not a figurative constant", k + 1)));
189                continue;
190            }
191            Expr::Operand(Operand::Literal(Literal::Alnum(_) | Literal::Hex(_) | Literal::National(_))) if arithmetic(formal.kind) => {
192                errors.push(Error::at(f.pos, format!("FUNCTION {name} argument {}: {} is numeric, and takes an argument COMPUTE could send it (assumption C272)", k + 1, formal.name)));
193                continue;
194            }
195            Expr::Operand(Operand::Ref(r)) if r.refmod.is_none() => r,
196            _ => continue,
197        };
198        if let Ok(Resolved::Item(i)) = layout.resolve(&r.name, &r.qualifiers, r.pos)
199            && let Some(why) = conformance(layout, i, alphabetic.contains(&layout.items[i].pos), decimal_point_comma, formal)
200        {
201            errors.push(Error::at(r.pos, format!("FUNCTION {name} argument {} ({}): {why}", k + 1, r.name)));
202        }
203    }
204}
205
206/// A numeric item an arithmetic statement can send or receive: not an index or a numeric-edited
207/// item.
208fn arithmetic(kind: Kind) -> bool {
209    matches!(kind, Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::Float(_))
210}