Repository: kdr-aus/ogma (rust)

Issue:
[BUG] Type inference with dot operator assumes `Num` output for `TableRow` lhs
## Current Behaviour

For a table row `$row.column`, the expected output is a `Num`. This should be made more flexible to use type inference.

## Expected Behaviour

## Reproduction Steps
1. 

### Additional information


Pull request:
Dot op infer output
Fixes #70 

Patch:
diff --git a/RELEASE.md b/RELEASE.md
index 09d8761..c15934a 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -13,5 +13,7 @@
 - Flush `Write`r once finished writing. (https://github.com/kdr-aus/ogma/pull/78)
 - Fixes (https://github.com/kdr-aus/ogma/issues/15) with new compilation engine.
 - Output better error messages for unmatched commands (https://github.com/kdr-aus/ogma/pull/86)
+- Dot operator now _infers_ output type when used with `TableRow`s.
+    (https://github.com/kdr-aus/ogma/pull/87)
 
 **✨ Other Updates**
diff --git a/ogma/src/lang/impls/intrinsics/pipeline.rs b/ogma/src/lang/impls/intrinsics/pipeline.rs
index d6cfacf..abdd5dd 100644
--- a/ogma/src/lang/impls/intrinsics/pipeline.rs
+++ b/ogma/src/lang/impls/intrinsics/pipeline.rs
@@ -189,7 +189,7 @@ impl FieldAccessor {
 impl ast::DotOperatorBlock {
     fn help() -> HelpMessage {
         HelpMessage {
-            desc: "extract a value out of a structure using infix operator".into(),
+            desc: "extract a value out of a structure using an infix operator".into(),
             params: vec![HelpParameter::Required("=> $foo.bar".into())],
             examples: vec![
                 HelpExample {
@@ -200,6 +200,10 @@ impl ast::DotOperatorBlock {
                     desc: "get the value of a column entry in a TableRow",
                     code: "$table-row.col-name",
                 },
+                HelpExample {
+                    desc: "explicitly constrain output type of a column",
+                    code: "$table-row.col-name:Str",
+                },
             ],
             ..HelpMessage::new(".")
         }
@@ -213,8 +217,11 @@ impl ast::DotOperatorBlock {
         match input.out_ty() {
             Ty::TabRow => {
                 let colarg = field.returns(Ty::Str)?.concrete()?;
-                // TODO remove type flag and use type infererence
-                let ty = TableGetType::Flag(Type::Num); // '.' does not support flags
+                let ty = blk
+                    .output_ty()
+                    .ok_or_else(|| Error::unknown_blk_output_type(blk.blk_tag()))?;
+                let ty = TableGetType::Flag(ty);
+
                 blk.eval(ty.ty().clone(), move |lhs_input, cx| {
                     let trow: TableRow = input.resolve(|| lhs_input, &cx)?.try_into()?;
                     table_row_get(&trow, &colarg, &ty, cx)


Test patch:
diff --git a/ogma/tests/commands/annotation.rs b/ogma/tests/commands/annotation.rs
index 8333a3a..d59add2 100644
--- a/ogma/tests/commands/annotation.rs
+++ b/ogma/tests/commands/annotation.rs
@@ -186,18 +186,14 @@ fn errors_02() {
         .unwrap_err()
         .to_string();
     println!("{}", x);
-    // NOTE: This will change once the dot operator switches to using type inference rather than
-    // default to a number
     assert_eq!(
         &x,
-        "Typing Error: Type application failed
---> shell:14
- | nth 1 Tuple #i.snd:Str #i.snd:Num
- |               ^ this node is trying to have a type `Number` applied to it
---> shell:14
+        "Evaluation Error: table entry for [row:2,col:'snd'] did not have expected type
+expected `String`, found `Number`
+--> shell:15
  | nth 1 Tuple #i.snd:Str #i.snd:Num
- |               ^ but it is already obligated to use type `String`
---> help: maybe remove any type annotations
+ |                ^^^
+--> help: column entries must have a matching type
 "
     );
 }
diff --git a/ogma/tests/commands/morphism.rs b/ogma/tests/commands/morphism.rs
index 1c519e2..aa06ac1 100644
--- a/ogma/tests/commands/morphism.rs
+++ b/ogma/tests/commands/morphism.rs
@@ -481,7 +481,7 @@ fn fold_while_test() {
     assert_eq!(x, Ok(Value::Num(36.into())));
     let x = process_w_num("range 1 20 | fold-while 1 {< 1e6} * $row.i", defs);
     assert_eq!(x, Ok(Value::Num((3628800).into())));
-    let x = process_w_num("range 1 20 | fold-while {Tuple 0 0} {get t0 | < 20} { let {get t0} $t0 | get t1 | + $t0 | Tuple #i $row.i } | get t0", defs);
+    let x = process_w_num("range 1 20 | fold-while {Tuple 0 0} {get t0 | < 20} { let {get t0} $t0 | get t1 | + $t0 | Tuple #i $row.i:Num } | get t0", defs);
     assert_eq!(x, Ok(Value::Num((21).into())));
 }
 
@@ -635,7 +635,7 @@ fn map_testing() {
         vec![n(-29), n(100), o("z")],
     ];
     check_is_table(x, exp);
-    let x = process_w_table("map 'Heading 3' --Str { \\$row.first | + 1 }", defs);
+    let x = process_w_table("map 'Heading 3' --Str { \\$row.first:Num | + 1 }", defs);
     let exp = vec![
         vec![o("first"), o("snd"), o("Heading 3")],
         vec![n(0), n(3), n(1)],
@@ -645,7 +645,10 @@ fn map_testing() {
     check_is_table(x, exp);
 
     // check we can use variables/exprs in column heading
-    let x = process_w_table("map { \\ 'Heading 3' } --Str { \\$row.first | + 1 }", defs);
+    let x = process_w_table(
+        "map { \\ 'Heading 3' } --Str { \\$row.first:Num | + 1 }",
+        defs,
+    );
     let exp = vec![
         vec![o("first"), o("snd"), o("Heading 3")],
         vec![n(0), n(3), n(1)],
@@ -654,7 +657,7 @@ fn map_testing() {
     ];
     check_is_table(x, exp);
     let x = process_w_table(
-        "let {\\ 'Heading 3'} $x | map $x --Str { \\$row.first | + 1 }",
+        "let {\\ 'Heading 3'} $x | map $x --Str { \\$row.first:Num | + 1 }",
         defs,
     );
     let exp = vec![
@@ -675,7 +678,7 @@ fn map_testing() {
     check_is_table(x, exp);
 
     // check ignore flag
-    let x = process_w_table("map 'Heading 3' --force $row.first", defs);
+    let x = process_w_table("map 'Heading 3' --force $row.first:Num", defs);
     let exp = vec![
         vec![o("first"), o("snd"), o("Heading 3")],
         vec![n(0), n(3), n(0)],
diff --git a/ogma/tests/commands/pipeline.rs b/ogma/tests/commands/pipeline.rs
index ea02951..6a969da 100644
--- a/ogma/tests/commands/pipeline.rs
+++ b/ogma/tests/commands/pipeline.rs
@@ -9,7 +9,7 @@ fn dotop_help_msg() {
         &x,
         "Help: `.`
 --> shell:0
- | extract a value out of a structure using infix operator
+ | extract a value out of a structure using an infix operator
  | 
  | Usage:
  |  => . => $foo.bar
@@ -20,6 +20,9 @@ fn dotop_help_msg() {
  | 
  |  get the value of a column entry in a TableRow
  |  => $table-row.col-name
+ | 
+ |  explicitly constrain output type of a column
+ |  => $table-row.col-name:Str
 "
     );
 }
@@ -187,6 +190,47 @@ fn dotop_err_test() {
     );
 }
 
+#[test]
+fn dotop_infers_output() {
+    let defs = &Definitions::new();
+
+    let x = process_w_table("fold 0 + $row.first", defs);
+    assert_eq!(x, Ok(Value::Num((-29.0).into())));
+
+    let x = process_w_table("fold 'foo' + $row.'Heading 3'", defs);
+    assert_eq!(x, Ok(Value::Str("fooabz".into())));
+
+    let x = process_w_table("fold 1 + $row.'Heading 3'", defs)
+        .unwrap_err()
+        .to_string();
+    println!("{}", x);
+    assert_eq!(
+        &x,
+        "Evaluation Error: table entry for [row:1,col:'Heading 3'] did not have expected type
+expected `Number`, found `String`
+--> shell:15
+ | fold 1 + $row.'Heading 3'
+ |                ^^^^^^^^^
+--> help: column entries must have a matching type
+"
+    );
+
+    let x = process_w_table("fold 'foo' + $row.first", defs)
+        .unwrap_err()
+        .to_string();
+    println!("{}", x);
+    assert_eq!(
+        &x,
+        "Evaluation Error: table entry for [row:1,col:'first'] did not have expected type
+expected `String`, found `Number`
+--> shell:18
+ | fold 'foo' + $row.first
+ |                   ^^^^^
+--> help: column entries must have a matching type
+"
+    );
+}
+
 // ------ Get ------------------------------------------------------------------
 #[test]
 fn get_help_msg() {
@@ -334,7 +378,7 @@ fn using_pound_i() {
 fn using_pound_i_table_row() {
     let d = &Definitions::new();
 
-    let x = process_w_table("append { \\ #i.snd | + 1 }", d);
+    let x = process_w_table("append { \\ #i.snd:Num | + 1 }", d);
     let exp = vec![
         vec![o("first"), o("snd"), o("Heading 3"), o("_append1")],
         vec![n(0), n(3), o("a"), n(4)],
@@ -489,7 +533,7 @@ fn let_tablerow() {
     ];
     let x = process_w_table("filter { let $x | get snd | > 10 }", defs);
     check_is_table(x, exp.clone());
-    let x = process_w_table("filter { let $x | \\$x.snd | > 10 }", defs);
+    let x = process_w_table("filter { let $x | \\$x.snd:Num | > 10 }", defs);
     check_is_table(x, exp);
 }