diff --git a/community/knowledge/performance/boolean-operators-do-not-short-circuit.bad.al b/community/knowledge/performance/boolean-operators-do-not-short-circuit.bad.al new file mode 100644 index 0000000..73b92db --- /dev/null +++ b/community/knowledge/performance/boolean-operators-do-not-short-circuit.bad.al @@ -0,0 +1,18 @@ +codeunit 50541 "Perf Sample NoShortCircuit Bad" +{ + procedure ExceedsThreshold(var Thresholds: array[10] of Decimal; Index: Integer; Amount: Decimal): Boolean + begin + // Thresholds[Index] is evaluated even when Index is 0, so the leading range + // check does not prevent the subscript from being read out of range. + exit((Index >= 1) and (Index <= ArrayLen(Thresholds)) and (Amount > Thresholds[Index])); + end; + + procedure IsBlockedCustomer(CustomerNo: Code[20]): Boolean + var + Customer: Record Customer; + begin + // The Get runs even for an empty CustomerNo, and Blocked is read even when the + // Get failed, so the result is taken from a record that was never loaded. + exit((CustomerNo <> '') and Customer.Get(CustomerNo) and (Customer.Blocked <> Customer.Blocked::" ")); + end; +} diff --git a/community/knowledge/performance/boolean-operators-do-not-short-circuit.good.al b/community/knowledge/performance/boolean-operators-do-not-short-circuit.good.al new file mode 100644 index 0000000..7d036c7 --- /dev/null +++ b/community/knowledge/performance/boolean-operators-do-not-short-circuit.good.al @@ -0,0 +1,24 @@ +codeunit 50540 "Perf Sample NoShortCircuit Good" +{ + procedure ExceedsThreshold(var Thresholds: array[10] of Decimal; Index: Integer; Amount: Decimal): Boolean + begin + // 'and' is safe here: both operands are cheap and neither depends on the other. + if (Index >= 1) and (Index <= ArrayLen(Thresholds)) then + // The subscript lives in its own if, so it is never evaluated out of range. + if Amount > Thresholds[Index] then + exit(true); + exit(false); + end; + + procedure IsBlockedCustomer(CustomerNo: Code[20]): Boolean + var + Customer: Record Customer; + begin + // The cheap test runs first, and the field is read only after Get succeeded. + if CustomerNo = '' then + exit(false); + if not Customer.Get(CustomerNo) then + exit(false); + exit(Customer.Blocked <> Customer.Blocked::" "); + end; +} diff --git a/community/knowledge/performance/boolean-operators-do-not-short-circuit.md b/community/knowledge/performance/boolean-operators-do-not-short-circuit.md new file mode 100644 index 0000000..96a0bde --- /dev/null +++ b/community/knowledge/performance/boolean-operators-do-not-short-circuit.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: performance +keywords: [short-circuit, lazy-evaluation, boolean-operators, nested-if, guard, and-operator, or-operator] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# AL boolean operators do not short-circuit + +## Description + +AL gives no short-circuit (lazy) evaluation guarantee for `and`, `or`, and `xor`: every operand of a boolean expression is evaluated, even when the leftmost operand already determines the result. Neither the AL operators documentation nor the boolean operators documentation defines a lazy evaluation order, so code must not depend on one. Developers arriving from C#, JavaScript, or SQL routinely assume the left operand guards the right; in AL it does not. The right operand still runs, so its cost is paid on every evaluation, and a check intended to protect an unsafe expression — an array subscript, a division, a field read that is only valid after a successful `Get` — does not protect it. + +## Best Practice + +Split a condition into nested `if` statements whenever one operand is only safe or only worth evaluating once another has passed. The guarding or cheapest condition goes in the outer `if`, the dependent or expensive one in the inner `if`, which makes the dependency explicit and keeps the expensive operand off the path that would have been skipped. Where there is no `else` branch, nesting is a pure win; where there is one, extract the conditions into a helper procedure that exits early instead. Keep `and` and `or` for operands that are independently safe and cheap — in-memory field comparisons, enum tests, bound checks — where combining them reads better and costs nothing. + +See sample: `boolean-operators-do-not-short-circuit.good.al`. + +## Anti Pattern + +A single condition that joins a guard with an operand depending on that guard, or with an expensive operand, using `and` or `or`. The consequence is either wasted work on every evaluation — a database call or validation procedure invoked even when the outcome is already decided — or a runtime error or silently wrong result that the guard was written to prevent. Detection signals: an operand that indexes an array or list with a variable whose bounds are checked in a sibling operand; `Record.Get(...)` or a `Find`/`IsEmpty` call as one operand of `and` with a field read of the same record as another; a boolean-returning procedure call combined with a cheap field test. The pattern is common in code ported from a language that does short-circuit, and in conditions grown by appending a clause to an existing `if`. + +See sample: `boolean-operators-do-not-short-circuit.bad.al`. + +## See also + +`microsoft/knowledge/performance/apply-guards-before-get.md` covers the related ordering rule for statements rather than operands.