Skip to content

Commit cbb95d8

Browse files
committed
fix bad expr issue
1 parent 276a859 commit cbb95d8

10 files changed

Lines changed: 35 additions & 27 deletions

File tree

demo/basic_demo/hello.sans

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ datasource in = inline_csv do
77
end
88

99
table t = from(in) do
10-
derive(base2 = a * 2)
11-
filter(base2 > 10)
12-
select a, base2
10+
derive(base2 = a * 2)
11+
filter(base2 > 10)
12+
select a, base2
1313
end
1414

1515
t select a, base2

demo/hello.sans

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ datasource in = inline_csv do
77
end
88

99
table t = from(in) do
10-
derive(base2 = a * 2)
11-
filter(base2 > 10)
12-
select a, base2
10+
derive(base2 = a * 2)
11+
filter(base2 > 10)
12+
select a, base2
1313
end
1414

1515
t select a, base2

demo/run_diff_clin_demo/dh_out/inputs/source/expanded.sans

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# sans 0.1
22
datasource lb = csv("lb.csv", columns(USUBJID, VISITNUM, LBDTC, LBTESTCD, LBORRES, LBSTRESN, LBSTRESU))
33
table __t2__ = from(lb)
4-
table __t3__ = __t2__ filter((LBTESTCD = "HBA1C"))
4+
table __t3__ = __t2__ filter((LBTESTCD == "HBA1C"))
55
table __t4__ = __t3__ rename(LBSTRESN -> A1C)
66
table __t5__ = __t4__ select USUBJID, VISITNUM, LBDTC, A1C
77
table __t6__ = __t5__ derive(label = "HIGH")

demo/run_diff_clin_demo/dh_out/report.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"role": "expanded",
1616
"name": "expanded.sans",
1717
"path": "inputs/source/expanded.sans",
18-
"sha256": "8ee2b5955a150e469790513f3315b46e55fee92b9735ee206a2e2cdf460d77b7"
18+
"sha256": "b82be58593d60a1fbd0194e6c95d21ffc89cbc74e24c609c9148d22380136916"
1919
}
2020
],
2121
"artifacts": [
@@ -93,13 +93,13 @@
9393
"timing": {
9494
"compile_ms": 3,
9595
"validate_ms": 0,
96-
"execute_ms": 4
96+
"execute_ms": 5
9797
},
98-
"report_sha256": "036ebc06865a15c377b9b1a19a479dfa6aead367e24b17c435e92e945807afb6",
98+
"report_sha256": "dee62c0f9d6dd407b3cbe6d4eab42d9bc7c94e6912486347b7e46650711b3247",
9999
"runtime": {
100100
"status": "ok",
101101
"timing": {
102-
"execute_ms": 4
102+
"execute_ms": 5
103103
}
104104
}
105105
}

demo/run_diff_clin_demo/dl_out/inputs/source/expanded.sans

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# sans 0.1
22
datasource lb = csv("lb.csv", columns(USUBJID, VISITNUM, LBDTC, LBTESTCD, LBORRES, LBSTRESN, LBSTRESU))
33
table __t2__ = from(lb)
4-
table __t3__ = __t2__ filter((LBTESTCD = "HBA1C"))
4+
table __t3__ = __t2__ filter((LBTESTCD == "HBA1C"))
55
table __t4__ = __t3__ rename(LBSTRESN -> A1C)
66
table __t5__ = __t4__ select USUBJID, VISITNUM, LBDTC, A1C
77
table __t6__ = __t5__ derive(label = "LOW")

demo/run_diff_clin_demo/dl_out/report.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"role": "expanded",
1616
"name": "expanded.sans",
1717
"path": "inputs/source/expanded.sans",
18-
"sha256": "0b1a75b07b02de945c8dded35d510880b19188b87eae828e338653de7d3bbd64"
18+
"sha256": "3f89722b1e1297609b0a08f59b5759b8413505becd9178e882be1ee75feabd53"
1919
}
2020
],
2121
"artifacts": [
@@ -95,7 +95,7 @@
9595
"validate_ms": 0,
9696
"execute_ms": 4
9797
},
98-
"report_sha256": "b46f430e9534fabebfd545e5db3f39d5f2dfadcece58c16e57fdfddd96fc230f",
98+
"report_sha256": "8d9b8ffcff2373c20407d041a80a53cf457edcbc3acbc9fbef412f21ceec1a78",
9999
"runtime": {
100100
"status": "ok",
101101
"timing": {

sans/sans/parser_expr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ def parse_expression(self, min_precedence: int = 0) -> ExprNode:
196196
"^=": "!=",
197197
"~=": "!=",
198198
"ne": "!=",
199-
"eq": "=",
200-
"==": "=",
199+
"eq": "==",
200+
"==": "==",
201201
"lt": "<",
202202
"le": "<=",
203203
"gt": ">",

sans/sans/runtime.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def _parse_value(raw: str) -> Any:
8080

8181
def _compare_sas(left: Any, right: Any, op: str) -> bool:
8282
"""Implements SAS-style comparison where None (missing) is smallest."""
83-
if op == "=": return left == right
83+
if op in {"=", "=="}: return left == right
8484
if op == "!=": return left != right
8585

8686
# Missing value logic for <, <=, >, >=
@@ -425,7 +425,7 @@ def _eval_expr(node: Dict[str, Any], row: Dict[str, Any], formats: Optional[Dict
425425
return left * right
426426
if op == "/":
427427
return left / right
428-
if op in {"=", "!=", "<", "<=", ">", ">="}:
428+
if op in {"=", "==", "!=", "<", "<=", ">", ">="}:
429429
return _compare_sas(left, right, op)
430430
raise RuntimeFailure(
431431
"SANS_RUNTIME_UNSUPPORTED_EXPR_NODE",
@@ -494,7 +494,7 @@ def _eval_expr_assert(
494494
op = node.get("op")
495495
left = _eval_expr_assert(node.get("left"), tables, formats)
496496
right = _eval_expr_assert(node.get("right"), tables, formats)
497-
if op in {"=", "!=", "<", "<=", ">", ">="}:
497+
if op in {"=", "==", "!=", "<", "<=", ">", ">="}:
498498
return _compare_sas(left, right, op)
499499
if op in {"+", "-", "*", "/"}:
500500
if left is None or right is None:
@@ -568,7 +568,7 @@ def _eval_expr_sql(node: Dict[str, Any], row: Dict[str, Any], col_map: Dict[str,
568568
return left * right
569569
if op == "/":
570570
return left / right
571-
if op in {"=", "!=", "<", "<=", ">", ">="}:
571+
if op in {"=", "==", "!=", "<", "<=", ">", ">="}:
572572
return _compare_sas(left, right, op)
573573
raise RuntimeFailure(
574574
"SANS_RUNTIME_UNSUPPORTED_EXPR_NODE",

sans/sans/sans_script/parser.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,16 +1200,16 @@ def _collect_block_by_parens(self, header: _Line, start_word: str) -> tuple[List
12001200
def _ensure_sans_expr_rules(self, text: str, line_no: int) -> None:
12011201
if not self._file_name:
12021202
return
1203-
# Basic check for == and !=
1203+
# Basic check for comparison operators (sans uses symbolic operators only)
12041204
# This is a bit naive if they are inside strings, but tokenize should handle it.
12051205
for token in tokenize(text, self._file_name):
12061206
if token.type != "OPERATOR":
12071207
continue
12081208
op = token.value.lower()
1209-
if op in {"=", "^=", "~=", "eq", "ne"}:
1209+
if op in {"=", "^=", "~=", "eq", "ne", "lt", "le", "gt", "ge"}:
12101210
raise SansScriptError(
12111211
code="E_BAD_EXPR",
1212-
message="Use '==' for equality and '!=' for inequality in sans scripts.",
1212+
message="Use symbolic comparison operators (==, !=, <, <=, >, >=) in sans scripts.",
12131213
line=line_no,
12141214
)
12151215

sans/tests/test_parser_expr.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
from sans.parser_expr import parse_expression_from_string
33
from sans.expr import lit, col, binop, boolop, unop, call
4+
from sans.sans_script import SansScriptError, parse_sans_script
45

56
def test_parse_literal_number():
67
assert parse_expression_from_string("123") == lit(123)
@@ -41,17 +42,24 @@ def test_parse_binary_operators_precedence():
4142
)
4243

4344
def test_parse_comparisons():
44-
assert parse_expression_from_string("a = b") == binop("=", col("a"), col("b"))
45-
assert parse_expression_from_string("a == b") == binop("=", col("a"), col("b"))
45+
assert parse_expression_from_string("a == b") == binop("==", col("a"), col("b"))
46+
assert parse_expression_from_string("a != b") == binop("!=", col("a"), col("b"))
4647
assert parse_expression_from_string("a > 10") == binop(">", col("a"), lit(10))
4748
assert parse_expression_from_string("x ~= y") == binop("!=", col("x"), col("y"))
48-
assert parse_expression_from_string("x != y") == binop("!=", col("x"), col("y"))
4949
assert parse_expression_from_string("a ne b") == binop("!=", col("a"), col("b"))
50-
assert parse_expression_from_string("a eq b") == binop("=", col("a"), col("b"))
50+
assert parse_expression_from_string("a eq b") == binop("==", col("a"), col("b"))
5151
assert parse_expression_from_string("a lt b") == binop("<", col("a"), col("b"))
5252
assert parse_expression_from_string("a le b") == binop("<=", col("a"), col("b"))
5353
assert parse_expression_from_string("a gt b") == binop(">", col("a"), col("b"))
5454
assert parse_expression_from_string("a ge b") == binop(">=", col("a"), col("b"))
55+
with pytest.raises(SansScriptError) as exc_info:
56+
parse_sans_script(
57+
"# sans 0.1\n"
58+
"let x = a = b\n"
59+
"x\n",
60+
"bad_expr.sans",
61+
)
62+
assert exc_info.value.code == "E_BAD_EXPR"
5563

5664
def test_parse_logical_operators_precedence():
5765
# if a > 1 and b < 2 works

0 commit comments

Comments
 (0)