libjay 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {libjay-0.3.0 → libjay-0.3.1}/Cargo.lock +5 -5
  2. {libjay-0.3.0 → libjay-0.3.1}/Cargo.toml +1 -1
  3. {libjay-0.3.0 → libjay-0.3.1}/PKG-INFO +1 -1
  4. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/explain.rs +7 -6
  5. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/frontend/apl.rs +191 -31
  6. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/frontend/j.rs +3 -3
  7. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/frontend/mod.rs +49 -0
  8. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/ir.rs +192 -33
  9. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/lib.rs +1 -1
  10. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/verb.rs +45 -8
  11. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-python/src/lib.rs +32 -2
  12. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-testkit/src/eval.rs +23 -12
  13. {libjay-0.3.0 → libjay-0.3.1}/python/jay/__init__.py +2 -0
  14. {libjay-0.3.0 → libjay-0.3.1}/python/jay/lang.py +18 -0
  15. {libjay-0.3.0 → libjay-0.3.1}/LICENSE +0 -0
  16. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/Cargo.toml +0 -0
  17. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/LICENSE +0 -0
  18. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/README.md +0 -0
  19. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/array.rs +0 -0
  20. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/complex.rs +0 -0
  21. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/device/codegen.rs +0 -0
  22. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/device/gpu.rs +0 -0
  23. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/device/mod.rs +0 -0
  24. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/dtype.rs +0 -0
  25. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/error.rs +0 -0
  26. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/exact.rs +0 -0
  27. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/fmt.rs +0 -0
  28. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/fuse.rs +0 -0
  29. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/gerund.rs +0 -0
  30. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/limits.rs +0 -0
  31. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/par.rs +0 -0
  32. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/rng.rs +0 -0
  33. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/simd.rs +0 -0
  34. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/sparse.rs +0 -0
  35. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay/src/symbol.rs +0 -0
  36. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-python/Cargo.toml +0 -0
  37. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-python/src/data.rs +0 -0
  38. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-testkit/Cargo.toml +0 -0
  39. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-testkit/src/compare.rs +0 -0
  40. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-testkit/src/corpus.rs +0 -0
  41. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-testkit/src/dialect.rs +0 -0
  42. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-testkit/src/lib.rs +0 -0
  43. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-testkit/src/replay.rs +0 -0
  44. {libjay-0.3.0 → libjay-0.3.1}/crates/libjay-testkit/src/snapshot.rs +0 -0
  45. {libjay-0.3.0 → libjay-0.3.1}/pyproject.toml +0 -0
  46. {libjay-0.3.0 → libjay-0.3.1}/python/README.md +0 -0
  47. {libjay-0.3.0 → libjay-0.3.1}/python/jay/_cli.py +0 -0
  48. {libjay-0.3.0 → libjay-0.3.1}/python/jay/_tstring.py +0 -0
@@ -494,7 +494,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
494
494
 
495
495
  [[package]]
496
496
  name = "libjay"
497
- version = "0.3.0"
497
+ version = "0.3.1"
498
498
  dependencies = [
499
499
  "libjay-testkit",
500
500
  "multiversion",
@@ -510,14 +510,14 @@ dependencies = [
510
510
 
511
511
  [[package]]
512
512
  name = "libjay-capi"
513
- version = "0.3.0"
513
+ version = "0.3.1"
514
514
  dependencies = [
515
515
  "libjay",
516
516
  ]
517
517
 
518
518
  [[package]]
519
519
  name = "libjay-devtools"
520
- version = "0.3.0"
520
+ version = "0.3.1"
521
521
  dependencies = [
522
522
  "libjay",
523
523
  "libjay-testkit",
@@ -526,7 +526,7 @@ dependencies = [
526
526
 
527
527
  [[package]]
528
528
  name = "libjay-python"
529
- version = "0.3.0"
529
+ version = "0.3.1"
530
530
  dependencies = [
531
531
  "arrow-array",
532
532
  "arrow-buffer",
@@ -538,7 +538,7 @@ dependencies = [
538
538
 
539
539
  [[package]]
540
540
  name = "libjay-testkit"
541
- version = "0.3.0"
541
+ version = "0.3.1"
542
542
  dependencies = [
543
543
  "libjay",
544
544
  ]
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/libjay", "crates/libjay-python", "crates/libjay-testkit"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.0"
6
+ version = "0.3.1"
7
7
  edition = "2024"
8
8
  rust-version = "1.89"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libjay
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -254,12 +254,13 @@ fn control_lines(c: &Control, depth: usize, p: &Program, tr: &Trace, out: &mut S
254
254
  block("test", test, out);
255
255
  block("body", body, out);
256
256
  }
257
- Control::For { name, source, body } => {
258
- let _ = writeln!(
259
- out,
260
- "{pad}for over items{}",
261
- name.as_ref().map_or(String::new(), |n| format!(", item in {n}, index in {n}_index"))
262
- );
257
+ Control::For { names, source, body } => {
258
+ let bound = match names.as_slice() {
259
+ [] => String::new(),
260
+ [n] => format!(", item in {n}, index in {n}_index"),
261
+ many => format!(", item taken apart into {}", many.join(" ")),
262
+ };
263
+ let _ = writeln!(out, "{pad}for over items{bound}");
263
264
  block("source", std::slice::from_ref(source), out);
264
265
  block("body", body, out);
265
266
  }
@@ -12,8 +12,8 @@ use std::sync::Arc;
12
12
  use crate::array::{Array, Data};
13
13
  use crate::error::{Error, Result, Span};
14
14
  use crate::frontend::{
15
- DefaultArg, DepthSign, DfnResult, FirstDisclose, IndexForm, LookupLeft, NestedModel, Partition,
16
- Rules, Segment, SourceParts,
15
+ ControlStrictness, DefaultArg, DepthSign, DfnResult, FirstDisclose, IndexForm, LookupLeft,
16
+ NestedModel, Partition, Rules, Segment, SourceParts,
17
17
  };
18
18
  use crate::ir::{Branch, Control, ExplicitDef, Expr, Scope};
19
19
  use crate::verb::{
@@ -28,6 +28,9 @@ use crate::verb::{
28
28
  pub fn parse(src: &SourceParts, d: Rules) -> Result<Vec<Expr>> {
29
29
  let sentences = lex(src, d)?;
30
30
  let mut verbs: HashMap<String, Verb> = HashMap::new();
31
+ for name in fixed_names(&sentences, d) {
32
+ verbs.entry(name.clone()).or_insert_with(|| Verb::Named(name.clone()));
33
+ }
31
34
  let mut stmts = Vec::with_capacity(sentences.len());
32
35
  let mut i = 0usize;
33
36
  while i < sentences.len() {
@@ -36,6 +39,22 @@ pub fn parse(src: &SourceParts, d: Rules) -> Result<Vec<Expr>> {
36
39
  stmts.push(stmt);
37
40
  continue;
38
41
  }
42
+ // A control structure standing on its own, outside any definition:
43
+ // its sentences are the lines of a body with nothing around them.
44
+ if let Some(Tok::Control(w)) = sentences[i].first().map(|t| &t.kind)
45
+ && matches!(*w, "If" | "While" | "Repeat" | "For" | "Select")
46
+ {
47
+ let end = control_block_end(&sentences, i).unwrap_or(sentences.len());
48
+ let mut items = Vec::with_capacity(end - i);
49
+ for line in &sentences[i..end] {
50
+ let mut label = None;
51
+ items.push(to_item(line.clone(), d, &mut verbs, &mut label)?);
52
+ }
53
+ let mut cursor = AplCursor { items: &items, at: 0, d, loops: 0 };
54
+ stmts.push(parse_apl_control(&mut cursor)?);
55
+ i = end;
56
+ continue;
57
+ }
39
58
  let mut sentence = sentences[i].clone();
40
59
  i += 1;
41
60
  // `⎕FX` defines a function where it stands; the sentence keeps only
@@ -57,6 +76,67 @@ pub fn parse(src: &SourceParts, d: Rules) -> Result<Vec<Expr>> {
57
76
  Ok(stmts)
58
77
  }
59
78
 
79
+ /// The functions the program fixes anywhere in it, by name.
80
+ ///
81
+ /// APL settles a name's class when the line runs; libjay compiles first, so
82
+ /// a definition whose body calls a function fixed AFTER it would have
83
+ /// nothing to call. The names every `∇` and `⎕FX` in the program gives a
84
+ /// function are therefore collected before anything is parsed, and each
85
+ /// stands as a verb resolved when it is applied — the definition it names
86
+ /// has been fixed by then. A header this cannot read is skipped: parsing
87
+ /// the definition itself reports the fault.
88
+ fn fixed_names(sentences: &[Vec<Token>], d: Rules) -> Vec<String> {
89
+ let mut out = Vec::new();
90
+ for line in sentences {
91
+ let head = match line.first().map(|t| &t.kind) {
92
+ Some(Tok::Del) => line[1..].to_vec(),
93
+ _ => {
94
+ let Some(at) = line.iter().position(|t| matches!(t.kind, Tok::QuadFx)) else {
95
+ continue;
96
+ };
97
+ let Some(Tok::Value(a)) = line.get(at + 1).map(|t| &t.kind) else {
98
+ continue;
99
+ };
100
+ let Data::Char(cs) = &a.data else { continue };
101
+ if a.rank() > 1 {
102
+ continue;
103
+ }
104
+ let text: String = cs.iter().collect();
105
+ let src = SourceParts::from_parts(&[&text], &[]);
106
+ match lex(&src, d) {
107
+ Ok(mut lexed) if lexed.len() == 1 => lexed.pop().unwrap_or_default(),
108
+ _ => continue,
109
+ }
110
+ }
111
+ };
112
+ let Some(first) = head.first() else { continue };
113
+ if let Ok((name, ..)) = parse_header(&head, first.span) {
114
+ out.push(name);
115
+ }
116
+ }
117
+ out
118
+ }
119
+
120
+ /// The sentence after the one that closes the control structure opening at
121
+ /// `at`. None where nothing closes it, which the block parser reports.
122
+ fn control_block_end(sentences: &[Vec<Token>], at: usize) -> Option<usize> {
123
+ let mut depth = 0usize;
124
+ for (k, line) in sentences.iter().enumerate().skip(at) {
125
+ let Some(Tok::Control(w)) = line.first().map(|t| &t.kind) else { continue };
126
+ match *w {
127
+ "If" | "While" | "Repeat" | "For" | "Select" => depth += 1,
128
+ "EndIf" | "EndWhile" | "EndFor" | "EndSelect" | "Until" | "End" => {
129
+ depth -= 1;
130
+ if depth == 0 {
131
+ return Some(k + 1);
132
+ }
133
+ }
134
+ _ => {}
135
+ }
136
+ }
137
+ None
138
+ }
139
+
60
140
  /// Where a `⎕FX` stands in a sentence of its own rather than inside a dfn's
61
141
  /// braces. One inside a body belongs to that body, which is compiled when
62
142
  /// the definition is, so it is left where it is and named there as a gap.
@@ -195,11 +275,11 @@ fn parse_statement(
195
275
  }
196
276
  return Ok(Some(Expr::VerbDef { name: n.clone(), verb: v, span }));
197
277
  }
278
+ // A control word that opens a structure is taken before the sentence
279
+ // is; anything left here continues or closes one that never opened.
198
280
  if let Some(t) = toks.iter().find(|t| matches!(t.kind, Tok::Control(_))) {
199
- return Err(Error::parse(
200
- "control structures are only meaningful inside a ∇ definition",
201
- t.span,
202
- ));
281
+ let Tok::Control(w) = t.kind else { unreachable!() };
282
+ return Err(Error::parse(format!(":{w} has no matching opening word"), t.span));
203
283
  }
204
284
  if let Some(t) = toks.iter().find(|t| matches!(t.kind, Tok::Arrow)) {
205
285
  return Err(Error::parse(
@@ -2434,9 +2514,10 @@ fn match_lparen(toks: &[Token], lo: usize, rparen: usize) -> Result<usize> {
2434
2514
  /// APL's control words, without their colon. `:End` closes any of the
2435
2515
  /// structures, which is the spelling GNU APL's manual gives as an
2436
2516
  /// alternative to the named closers.
2437
- const CONTROL_WORDS: [&str; 18] = [
2438
- "If", "ElseIf", "Else", "EndIf", "While", "EndWhile", "Repeat", "Until", "For", "In",
2439
- "EndFor", "Select", "Case", "EndSelect", "Return", "Leave", "Continue", "End",
2517
+ const CONTROL_WORDS: [&str; 21] = [
2518
+ "If", "ElseIf", "Else", "EndIf", "AndIf", "OrIf", "While", "EndWhile", "Repeat", "Until",
2519
+ "For", "In", "EndFor", "Select", "Case", "CaseList", "EndSelect", "Return", "Leave",
2520
+ "Continue", "End",
2440
2521
  ];
2441
2522
 
2442
2523
  /// The control word a `:name` spells, case-insensitively as the references
@@ -2812,7 +2893,7 @@ fn build_tradfn(
2812
2893
  items.push(item);
2813
2894
  }
2814
2895
  let item_count = items.len();
2815
- let mut cursor = AplCursor { items: &items, at: 0, d };
2896
+ let mut cursor = AplCursor { items: &items, at: 0, d, loops: 0 };
2816
2897
  let mut body = parse_apl_block(&mut cursor, &[])?;
2817
2898
  // A label is the number of a LINE, so the statements have to be the
2818
2899
  // lines: a control structure folds several of them into one and the
@@ -2968,6 +3049,9 @@ struct AplCursor<'a> {
2968
3049
  at: usize,
2969
3050
  /// The dialect, for the sentences a control word carries.
2970
3051
  d: Rules,
3052
+ /// How many loops enclose the position being parsed, which is what
3053
+ /// decides whether a `:Leave` has one to leave.
3054
+ loops: usize,
2971
3055
  }
2972
3056
 
2973
3057
  impl<'a> AplCursor<'a> {
@@ -2985,6 +3069,14 @@ impl<'a> AplCursor<'a> {
2985
3069
  .map_or_else(|| Span::new(0, 0), AplItem::span)
2986
3070
  }
2987
3071
 
3072
+ /// Parse a loop's body, with the loop counted around it.
3073
+ fn in_loop<T>(&mut self, f: impl FnOnce(&mut Self) -> Result<T>) -> Result<T> {
3074
+ self.loops += 1;
3075
+ let out = f(self);
3076
+ self.loops -= 1;
3077
+ out
3078
+ }
3079
+
2988
3080
  /// Consume the closing word, which may also be spelled `:End`.
2989
3081
  fn close(&mut self, want: &str) -> Result<()> {
2990
3082
  match self.peek_word() {
@@ -3031,8 +3123,14 @@ fn parse_apl_control(cur: &mut AplCursor<'_>) -> Result<Expr> {
3031
3123
  let mut test = rest;
3032
3124
  loop {
3033
3125
  let test_expr = condition(test, start, cur.d)?;
3126
+ let test_expr = continued_condition(cur, test_expr)?;
3034
3127
  let body = parse_apl_block(cur, &["ElseIf", "Else", "EndIf"])?;
3035
- arms.push(Branch { test: Some(vec![test_expr]), body, fall_through: false });
3128
+ arms.push(Branch {
3129
+ test: Some(vec![test_expr]),
3130
+ body,
3131
+ fall_through: false,
3132
+ list: false,
3133
+ });
3036
3134
  match cur.peek_word() {
3037
3135
  Some("ElseIf") => {
3038
3136
  let Some(AplItem::Word { rest, .. }) = cur.peek() else { unreachable!() };
@@ -3055,7 +3153,8 @@ fn parse_apl_control(cur: &mut AplCursor<'_>) -> Result<Expr> {
3055
3153
  }
3056
3154
  "While" => {
3057
3155
  let test = condition(rest, start, cur.d)?;
3058
- let body = parse_apl_block(cur, &["EndWhile"])?;
3156
+ let test = continued_condition(cur, test)?;
3157
+ let body = cur.in_loop(|cur| parse_apl_block(cur, &["EndWhile"]))?;
3059
3158
  cur.close("EndWhile")?;
3060
3159
  Control::While { test: vec![test], body, body_first: false, until: false }
3061
3160
  }
@@ -3063,40 +3162,50 @@ fn parse_apl_control(cur: &mut AplCursor<'_>) -> Result<Expr> {
3063
3162
  if !rest.is_empty() {
3064
3163
  return Err(Error::parse(":Repeat takes no condition", start));
3065
3164
  }
3066
- let body = parse_apl_block(cur, &["Until"])?;
3165
+ let body = cur.in_loop(|cur| parse_apl_block(cur, &["Until"]))?;
3067
3166
  let Some(AplItem::Word { rest, span, .. }) = cur.peek() else {
3068
3167
  return Err(Error::parse("this :Repeat needs an :Until", cur.last_span()));
3069
3168
  };
3070
- let test = condition(rest.clone(), *span, cur.d)?;
3169
+ let (rest, span) = (rest.clone(), *span);
3170
+ let test = condition(rest, span, cur.d)?;
3071
3171
  cur.at += 1;
3172
+ let test = continued_condition(cur, test)?;
3072
3173
  Control::While { test: vec![test], body, body_first: true, until: true }
3073
3174
  }
3074
3175
  "For" => {
3075
- // `:For name :In source`.
3076
- let (name, source) = for_header(&rest, start, cur.d)?;
3077
- let body = parse_apl_block(cur, &["EndFor"])?;
3176
+ // `:For name… :In source`.
3177
+ let (names, source) = for_header(&rest, start, cur.d)?;
3178
+ let body = cur.in_loop(|cur| parse_apl_block(cur, &["EndFor"]))?;
3078
3179
  cur.close("EndFor")?;
3079
- Control::For { name: Some(name), source: Box::new(source), body }
3180
+ Control::For { names, source: Box::new(source), body }
3080
3181
  }
3081
3182
  "Select" => {
3082
3183
  let subject = condition(rest, start, cur.d)?;
3083
3184
  let mut cases = Vec::new();
3084
3185
  loop {
3085
3186
  match cur.peek() {
3086
- Some(AplItem::Word { word: "Case", rest, span }) => {
3187
+ Some(AplItem::Word { word: w @ ("Case" | "CaseList"), rest, span }) => {
3188
+ let list = *w == "CaseList";
3087
3189
  let test = condition(rest.clone(), *span, cur.d)?;
3088
3190
  cur.at += 1;
3089
- let body = parse_apl_block(cur, &["Case", "Else", "EndSelect"])?;
3191
+ let body =
3192
+ parse_apl_block(cur, &["Case", "CaseList", "Else", "EndSelect"])?;
3090
3193
  cases.push(Branch {
3091
3194
  test: Some(vec![test]),
3092
3195
  body,
3093
3196
  fall_through: false,
3197
+ list,
3094
3198
  });
3095
3199
  }
3096
3200
  Some(AplItem::Word { word: "Else", .. }) => {
3097
3201
  cur.at += 1;
3098
3202
  let body = parse_apl_block(cur, &["EndSelect"])?;
3099
- cases.push(Branch { test: None, body, fall_through: false });
3203
+ cases.push(Branch {
3204
+ test: None,
3205
+ body,
3206
+ fall_through: false,
3207
+ list: false,
3208
+ });
3100
3209
  cur.close("EndSelect")?;
3101
3210
  break;
3102
3211
  }
@@ -3109,8 +3218,14 @@ fn parse_apl_control(cur: &mut AplCursor<'_>) -> Result<Expr> {
3109
3218
  Control::Select { subject: Box::new(subject), cases }
3110
3219
  }
3111
3220
  "Return" => Control::Return,
3112
- "Leave" => Control::Break,
3113
- "Continue" => Control::Continue,
3221
+ // Dyalog wants a loop around `:Leave` and `:Continue`; the lenient
3222
+ // reading lets a stray one leave the definition instead.
3223
+ "Leave" | "Continue" => {
3224
+ if cur.loops == 0 && cur.d.control_strictness == ControlStrictness::Strict {
3225
+ return Err(Error::parse(format!(":{word} belongs inside a loop"), start));
3226
+ }
3227
+ if word == "Leave" { Control::Break } else { Control::Continue }
3228
+ }
3114
3229
  other => {
3115
3230
  return Err(Error::parse(format!(":{other} has no matching opening word"), start));
3116
3231
  }
@@ -3118,6 +3233,46 @@ fn parse_apl_control(cur: &mut AplCursor<'_>) -> Result<Expr> {
3118
3233
  Ok(Expr::Control(Box::new(control), Span::merge(start, cur.last_span())))
3119
3234
  }
3120
3235
 
3236
+ /// The `:AndIf` and `:OrIf` lines that continue the condition above them.
3237
+ ///
3238
+ /// Both SHORT-CIRCUIT: the second test does not run where the first has
3239
+ /// settled the answer. A test is a block, whose value is its last
3240
+ /// sentence's, so the two cannot simply be listed; the continuation is an
3241
+ /// `:If` of its own instead, answering with the next test where the first
3242
+ /// leaves the question open and with the settled truth value where it does
3243
+ /// not. They chain left to right, `:AndIf` and `:OrIf` alike.
3244
+ fn continued_condition(cur: &mut AplCursor<'_>, mut test: Expr) -> Result<Expr> {
3245
+ loop {
3246
+ let (word, rest, span) = match cur.peek() {
3247
+ Some(AplItem::Word { word: w @ ("AndIf" | "OrIf"), rest, span }) => {
3248
+ (*w, rest.clone(), *span)
3249
+ }
3250
+ _ => return Ok(test),
3251
+ };
3252
+ cur.at += 1;
3253
+ let next = condition(rest, span, cur.d)?;
3254
+ let settled = Expr::Const(Array::scalar_bool(word == "OrIf"), span);
3255
+ let (body, otherwise) = if word == "AndIf" {
3256
+ (vec![next], vec![settled])
3257
+ } else {
3258
+ (vec![settled], vec![next])
3259
+ };
3260
+ let whole = Span::merge(test.span(), span);
3261
+ test = Expr::Control(
3262
+ Box::new(Control::If {
3263
+ arms: vec![Branch {
3264
+ test: Some(vec![test]),
3265
+ body,
3266
+ fall_through: false,
3267
+ list: false,
3268
+ }],
3269
+ otherwise: Some(otherwise),
3270
+ }),
3271
+ whole,
3272
+ );
3273
+ }
3274
+ }
3275
+
3121
3276
  /// The tokens after a control word, as one expression.
3122
3277
  fn condition(rest: Vec<Token>, span: Span, d: Rules) -> Result<Expr> {
3123
3278
  match rest.first() {
@@ -3135,23 +3290,28 @@ fn condition(rest: Vec<Token>, span: Span, d: Rules) -> Result<Expr> {
3135
3290
  }
3136
3291
  }
3137
3292
 
3138
- /// `:For name :In source`.
3139
- fn for_header(rest: &[Token], span: Span, d: Rules) -> Result<(String, Expr)> {
3140
- let Some(Tok::Name(name)) = rest.first().map(|t| &t.kind) else {
3141
- return Err(Error::parse(":For needs a name to bind", span));
3142
- };
3293
+ /// `:For name… :In source`. Several names take each item apart between
3294
+ /// them, one of its own items each.
3295
+ fn for_header(rest: &[Token], span: Span, d: Rules) -> Result<(Vec<String>, Expr)> {
3143
3296
  let Some(k) = rest.iter().position(|t| matches!(t.kind, Tok::Control("In"))) else {
3144
3297
  return Err(Error::parse(":For needs an :In", span));
3145
3298
  };
3146
- if k != 1 {
3147
- return Err(Error::not_yet("several :For names", span));
3299
+ let mut names = Vec::with_capacity(k);
3300
+ for t in &rest[..k] {
3301
+ match &t.kind {
3302
+ Tok::Name(n) => names.push(n.clone()),
3303
+ _ => return Err(Error::parse(":For binds names, one per item", t.span)),
3304
+ }
3305
+ }
3306
+ if names.is_empty() {
3307
+ return Err(Error::parse(":For needs a name to bind", span));
3148
3308
  }
3149
3309
  let source = &rest[k + 1..];
3150
3310
  let Some(first) = source.first() else {
3151
3311
  return Err(Error::parse(":In needs a value", span));
3152
3312
  };
3153
3313
  let hint = Span::merge(first.span, source[source.len() - 1].span);
3154
- Ok((name.clone(), parse_prepared(source, hint, d)?))
3314
+ Ok((names, parse_prepared(source, hint, d)?))
3155
3315
  }
3156
3316
 
3157
3317
  /// Parse a token run that has already had its names and dfns folded.
@@ -841,7 +841,7 @@ fn parse_control(cur: &mut Cursor<'_>, scope: &mut Names) -> Result<Expr> {
841
841
  let body = parse_block(cur, scope, &["end."])?;
842
842
  let end = cur.expect("end.")?;
843
843
  let source = one_expr(source, Span::merge(start, end))?;
844
- Control::For { name: suffix.clone(), source: Box::new(source), body }
844
+ Control::For { names: suffix.iter().cloned().collect(), source: Box::new(source), body }
845
845
  }
846
846
  "select." => parse_select(cur, scope, start)?,
847
847
  "try." => {
@@ -883,7 +883,7 @@ fn parse_if(cur: &mut Cursor<'_>, scope: &mut Names) -> Result<Control> {
883
883
  let test = parse_block(cur, scope, &["do."])?;
884
884
  cur.expect("do.")?;
885
885
  let body = parse_block(cur, scope, &["elseif.", "else.", "end."])?;
886
- arms.push(Branch { test: Some(test), body, fall_through: false });
886
+ arms.push(Branch { test: Some(test), body, fall_through: false, list: false });
887
887
  match cur.peek_word() {
888
888
  Some("elseif.") => {
889
889
  cur.at += 1;
@@ -927,7 +927,7 @@ fn parse_select(cur: &mut Cursor<'_>, scope: &mut Names, start: Span) -> Result<
927
927
  let body = parse_block(cur, scope, &["case.", "fcase.", "end."])?;
928
928
  // `case. do.` with no test is the default arm.
929
929
  let test = (!test.is_empty()).then_some(test);
930
- cases.push(Branch { test, body, fall_through });
930
+ cases.push(Branch { test, body, fall_through, list: false });
931
931
  }
932
932
  Ok(Control::Select { subject: Box::new(subject), cases })
933
933
  }
@@ -219,6 +219,43 @@ pub enum EncodeDigits {
219
219
  Exact,
220
220
  }
221
221
 
222
+ /// How strictly a control structure reads what it is given.
223
+ ///
224
+ /// The lenient reading is the one both languages ship: a condition is true
225
+ /// where its first atom is, whatever else it holds, and a `:Leave` outside
226
+ /// a loop leaves the definition. Dyalog reads both strictly — a condition
227
+ /// is one element and no more, and `:Leave` belongs to a loop — and says so
228
+ /// rather than answering. GNU APL has no control structures at all, so
229
+ /// nothing it records turns on this.
230
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
231
+ pub enum ControlStrictness {
232
+ /// The first atom decides, and a stray `:Leave` is a return.
233
+ #[default]
234
+ Lenient,
235
+ /// Dyalog: a condition is a single value, and `:Leave` needs its loop.
236
+ Strict,
237
+ }
238
+
239
+ /// Where the each in the inner product's definition sits.
240
+ ///
241
+ /// `f.g` is a fold over a pairing, and the two lines put the each on
242
+ /// different halves of it. GNU APL puts it on the FOLD — `f/¨ (⊂[last]x)
243
+ /// ∘.g (⊂[first]y)` — so `g` meets one whole vector from each side and what
244
+ /// the fold makes of a pair is enclosed once more. Dyalog puts it on the
245
+ /// PAIRING — `f/ row g¨ column` — so `g` meets one element from each side
246
+ /// and the fold's own value stands as the cell. `1 2+.,3 4` is `10` under
247
+ /// the first and an enclosed `3 7` under the second. The two agree wherever
248
+ /// `g` is a scalar function and the fold ends in a number, which is every
249
+ /// published use, so `+.×` and the Life idiom's `∨.∧` differ only in depth.
250
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
251
+ pub enum InnerEach {
252
+ /// GNU APL: `f/¨` over the outer product.
253
+ #[default]
254
+ OnFold,
255
+ /// Dyalog: `f/` over `g¨`.
256
+ OnPair,
257
+ }
258
+
222
259
  /// Dialect settings supplied by the host.
223
260
  ///
224
261
  /// This is what a host asks for; [`Rules`] is what the compiler and the
@@ -252,6 +289,8 @@ pub struct Dialect {
252
289
  pub near_count: NearCount,
253
290
  pub floor_rule: FloorRule,
254
291
  pub encode_digits: EncodeDigits,
292
+ pub inner_each: InnerEach,
293
+ pub control_strictness: ControlStrictness,
255
294
  /// Whether a function may stand where a value belongs: a run of
256
295
  /// functions is then a train, and `F←+/` names one. Both readings are
257
296
  /// implemented, so this is a choice and not a gap. It ships on, as an
@@ -290,6 +329,8 @@ impl Dialect {
290
329
  near_count: NearCount::Absolute,
291
330
  floor_rule: FloorRule::Shift,
292
331
  encode_digits: EncodeDigits::Tolerant,
332
+ inner_each: InnerEach::OnFold,
333
+ control_strictness: ControlStrictness::Lenient,
293
334
  trains: true,
294
335
  }
295
336
  }
@@ -320,6 +361,8 @@ impl Dialect {
320
361
  near_count: NearCount::Tolerant,
321
362
  floor_rule: FloorRule::Scaled,
322
363
  encode_digits: EncodeDigits::Exact,
364
+ inner_each: InnerEach::OnPair,
365
+ control_strictness: ControlStrictness::Strict,
323
366
  trains: true,
324
367
  }
325
368
  }
@@ -395,6 +438,8 @@ impl Dialect {
395
438
  near_count: self.near_count,
396
439
  floor_rule: self.floor_rule,
397
440
  encode_digits: self.encode_digits,
441
+ inner_each: self.inner_each,
442
+ control_strictness: self.control_strictness,
398
443
  trains: self.trains,
399
444
  })
400
445
  }
@@ -427,6 +472,8 @@ pub struct Rules {
427
472
  pub near_count: NearCount,
428
473
  pub floor_rule: FloorRule,
429
474
  pub encode_digits: EncodeDigits,
475
+ pub inner_each: InnerEach,
476
+ pub control_strictness: ControlStrictness,
430
477
  pub trains: bool,
431
478
  }
432
479
 
@@ -456,6 +503,8 @@ impl Rules {
456
503
  near_count: self.near_count,
457
504
  floor_rule: self.floor_rule,
458
505
  encode_digits: self.encode_digits,
506
+ inner_each: self.inner_each,
507
+ control_strictness: self.control_strictness,
459
508
  trains: self.trains,
460
509
  }
461
510
  }