Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/semantic-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ Comparison operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) promote their operands
with the same table and always return `Boolean` semantically and `i1` in LLVM
IR.

## Boolean And Comparison Contract

Boolean behavior is part of the stable semantic boundary:

- comparisons always return `Boolean`
- `if`, `while`, and `for-count` conditions must be `Boolean`
- `&&`, `||`, and `!` require `Boolean` operands
- implicit truthiness is forbidden for integers, floats, pointers, and other
non-boolean values

Lowering should branch directly on the analyzed Boolean `i1` value for control
flow instead of inventing zero-comparison truthiness rules during codegen.

### Canonical Cast Policy

Implicit promotions in variable initializers, assignments, call arguments, and
Expand Down
30 changes: 29 additions & 1 deletion src/irx/analysis/handlers/control_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,34 @@
SemanticAnalyzerCore,
SemanticVisitorMixinBase,
)
from irx.analysis.types import is_assignable
from irx.analysis.types import is_assignable, is_boolean_type
from irx.typecheck import typechecked


@typechecked
class ControlFlowVisitorMixin(SemanticVisitorMixinBase):
def _validate_boolean_condition(
self,
condition: astx.AST,
*,
label: str,
) -> None:
"""
title: Validate a control-flow condition is Boolean.
parameters:
condition:
type: astx.AST
label:
type: str
"""
condition_type = self._expr_type(condition)
if condition_type is None or is_boolean_type(condition_type):
return
self.context.diagnostics.add(
f"{label} condition must be Boolean",
node=condition,
)

@SemanticAnalyzerCore.visit.dispatch
def visit(self, node: astx.FunctionReturn) -> None:
"""
Expand Down Expand Up @@ -54,6 +76,7 @@ def visit(self, node: astx.IfStmt) -> None:
type: astx.IfStmt
"""
self.visit(node.condition)
self._validate_boolean_condition(node.condition, label="if")
self.visit(node.then)
if node.else_ is not None:
self.visit(node.else_)
Expand All @@ -68,6 +91,7 @@ def visit(self, node: astx.WhileStmt) -> None:
type: astx.WhileStmt
"""
self.visit(node.condition)
self._validate_boolean_condition(node.condition, label="while")
with self.context.in_loop():
self.visit(node.body)
self._set_type(node, None)
Expand All @@ -93,6 +117,10 @@ def visit(self, node: astx.ForCountLoopStmt) -> None:
)
self._set_symbol(node.initializer, symbol)
self.visit(node.condition)
self._validate_boolean_condition(
node.condition,
label="for-count loop",
)
self.visit(node.update)
with self.context.in_loop():
self.visit(node.body)
Expand Down
20 changes: 20 additions & 0 deletions src/irx/analysis/handlers/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ def visit(self, node: astx.UnaryOp) -> None:
"""
self.visit(node.operand)
operand_type = self._expr_type(node.operand)
if (
node.op_code == "!"
and operand_type is not None
and not is_boolean_type(operand_type)
):
self.context.diagnostics.add(
"unary operator '!' requires Boolean operand",
node=node,
)
result_type = unary_result_type(node.op_code, operand_type)
if node.op_code in {"++", "--"} and isinstance(
node.operand, astx.Identifier
Expand Down Expand Up @@ -200,6 +209,17 @@ def visit(self, node: astx.BinaryOp) -> None:
if flags.fma and flags.fma_rhs is not None:
self.visit(flags.fma_rhs)

if (
node.op_code in {"&&", "and", "||", "or"}
and lhs_type is not None
and rhs_type is not None
and not (is_boolean_type(lhs_type) and is_boolean_type(rhs_type))
):
self.context.diagnostics.add(
f"logical operator '{node.op_code}' requires Boolean operands",
node=node,
)

if node.op_code in {"+", "-", "*", "/", "%"} and not (
(is_numeric_type(lhs_type) and is_numeric_type(rhs_type))
or (
Expand Down
6 changes: 2 additions & 4 deletions src/irx/analysis/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ def binary_result_type(
if op_code in {"&&", "and", "||", "or"}:
if is_boolean_type(lhs_type) and is_boolean_type(rhs_type):
return astx.Boolean()
if is_numeric_type(lhs_type) and is_numeric_type(rhs_type):
return common_numeric_type(lhs_type, rhs_type)
return lhs_type if lhs_type == rhs_type else None
return None

if op_code == "=":
return lhs_type
Expand Down Expand Up @@ -82,7 +80,7 @@ def unary_result_type(
if op_code == "!":
if is_boolean_type(operand_type):
return astx.Boolean()
return operand_type
return None
if op_code in {"++", "--"} and is_numeric_type(operand_type):
return operand_type
return operand_type
34 changes: 31 additions & 3 deletions src/irx/builder/lowering/binary_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
)
from irx.builder.protocols import VisitorMixinBase
from irx.builder.runtime import safe_pop
from irx.builder.types import is_fp_type
from irx.builder.types import is_fp_type, is_int_type
from irx.builder.vector import emit_add, emit_int_div, is_vector
from irx.typecheck import typechecked

Expand Down Expand Up @@ -153,6 +153,34 @@ def _emit_vector_add(
self.set_fast_math(prev_fast_math)
return result

def _load_boolean_operands(
self,
node: astx.BinaryOp,
) -> tuple[ir.Value, ir.Value]:
"""
title: Load Boolean operands for logical operators.
parameters:
node:
type: astx.BinaryOp
returns:
type: tuple[ir.Value, ir.Value]
"""
llvm_lhs, llvm_rhs, _unsigned = self._load_binary_operands(
node,
unify_numeric=False,
)
if (
not is_int_type(llvm_lhs.type)
or llvm_lhs.type.width != 1
or not is_int_type(llvm_rhs.type)
or llvm_rhs.type.width != 1
):
raise Exception(
"codegen: logical operator "
f"'{node.op_code}' must lower Boolean operands."
)
return llvm_lhs, llvm_rhs

def _emit_vector_sub(
self,
node: SubBinOp,
Expand Down Expand Up @@ -524,7 +552,7 @@ def visit(self, node: LogicalAndBinOp) -> None:
node:
type: LogicalAndBinOp
"""
llvm_lhs, llvm_rhs, _unsigned = self._load_binary_operands(node)
llvm_lhs, llvm_rhs = self._load_boolean_operands(node)
result = self._llvm.ir_builder.and_(llvm_lhs, llvm_rhs, "andtmp")
self.result_stack.append(result)

Expand All @@ -536,7 +564,7 @@ def visit(self, node: LogicalOrBinOp) -> None:
node:
type: LogicalOrBinOp
"""
llvm_lhs, llvm_rhs, _unsigned = self._load_binary_operands(node)
llvm_lhs, llvm_rhs = self._load_boolean_operands(node)
result = self._llvm.ir_builder.or_(llvm_lhs, llvm_rhs, "ortmp")
self.result_stack.append(result)

Expand Down
63 changes: 37 additions & 26 deletions src/irx/builder/lowering/control_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,37 @@
from irx.builder.core import VisitorCore, semantic_symbol_key
from irx.builder.protocols import VisitorMixinBase
from irx.builder.runtime import safe_pop
from irx.builder.types import is_fp_type
from irx.builder.types import is_fp_type, is_int_type
from irx.builder.vector import emit_add
from irx.typecheck import typechecked


@typechecked
class ControlFlowVisitorMixin(VisitorMixinBase):
def _lower_boolean_condition(
self,
value: ir.Value | None,
*,
context: str,
) -> ir.Value:
"""
title: Require one lowered control-flow condition to be Boolean.
parameters:
value:
type: ir.Value | None
context:
type: str
returns:
type: ir.Value
"""
if value is None:
raise Exception("codegen: Invalid condition expression.")
if not is_int_type(value.type) or value.type.width != 1:
raise Exception(
f"codegen: {context} condition must lower to Boolean."
)
return value

@VisitorCore.visit.dispatch
def visit(self, block: astx.Block) -> None:
"""
Expand Down Expand Up @@ -51,18 +75,10 @@ def visit(self, node: astx.IfStmt) -> None:
type: astx.IfStmt
"""
self.visit_child(node.condition)
cond_v = safe_pop(self.result_stack)
if cond_v is None:
raise Exception("codegen: Invalid condition expression.")

if is_fp_type(cond_v.type):
cmp_instruction = self._llvm.ir_builder.fcmp_ordered
zero_val = ir.Constant(cond_v.type, 0.0)
else:
cmp_instruction = self._llvm.ir_builder.icmp_signed
zero_val = ir.Constant(cond_v.type, 0)

cond_v = cmp_instruction("!=", cond_v, zero_val)
cond_v = self._lower_boolean_condition(
safe_pop(self.result_stack),
context="if",
)

then_bb = self._llvm.ir_builder.function.append_basic_block(
"bb_if_then"
Expand Down Expand Up @@ -155,18 +171,10 @@ def visit(self, expr: astx.WhileStmt) -> None:

self._llvm.ir_builder.position_at_end(cond_bb)
self.visit_child(expr.condition)
cond_val = safe_pop(self.result_stack)
if cond_val is None:
raise Exception("codegen: Invalid condition expression.")

if is_fp_type(cond_val.type):
cmp_instruction = self._llvm.ir_builder.fcmp_ordered
zero_val = ir.Constant(cond_val.type, 0.0)
else:
cmp_instruction = self._llvm.ir_builder.icmp_signed
zero_val = ir.Constant(cond_val.type, 0)

cond_val = cmp_instruction("!=", cond_val, zero_val, "whilecond")
cond_val = self._lower_boolean_condition(
safe_pop(self.result_stack),
context="while",
)
self._llvm.ir_builder.cbranch(cond_val, body_bb, after_bb)
self.loop_stack.append(
{
Expand Down Expand Up @@ -218,7 +226,10 @@ def visit(self, node: astx.ForCountLoopStmt) -> None:
self.named_values[initializer_key] = var_addr

self.visit_child(node.condition)
cond_val = safe_pop(self.result_stack)
cond_val = self._lower_boolean_condition(
safe_pop(self.result_stack),
context="for-count loop",
)

loop_body_bb = self._llvm.ir_builder.function.append_basic_block(
"loop.body"
Expand Down
20 changes: 9 additions & 11 deletions src/irx/builder/lowering/unary_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from irx.builder.core import VisitorCore, semantic_symbol_key
from irx.builder.protocols import VisitorMixinBase
from irx.builder.runtime import safe_pop
from irx.builder.types import is_fp_type
from irx.builder.types import is_fp_type, is_int_type
from irx.typecheck import typechecked


Expand Down Expand Up @@ -88,19 +88,17 @@ def visit(self, node: astx.UnaryOp) -> None:
val = safe_pop(self.result_stack)
if val is None:
raise Exception("codegen: Invalid unary operand.")
if not is_int_type(val.type) or val.type.width != 1:
raise Exception(
"codegen: unary operator '!' must lower a Boolean operand."
)

zero = ir.Constant(val.type, 0)
is_zero = self._llvm.ir_builder.icmp_signed(
"==", val, zero, "iszero"
result = self._llvm.ir_builder.xor(
val,
ir.Constant(self._llvm.BOOLEAN_TYPE, 1),
"nottmp",
)

if isinstance(val.type, ir.IntType) and val.type.width == 1:
result = is_zero
else:
result = self._llvm.ir_builder.zext(
is_zero, val.type, "nottmp"
)

if isinstance(node.operand, astx.Identifier):
operand_key = semantic_symbol_key(
node.operand, node.operand.name
Expand Down
Loading
Loading