How it works
Runtime and Types
Every value in a running kemlang-py program is one of five Python types. This page covers how those types behave, how coercion works, what is truthy, and the full lifecycle of a program from file read to process exit.
KemValue - the runtime type
kemlang-py defines a single type alias that covers all possible runtime values:
KemValue = int | float | str | bool | None
There are no wrapper classes. No KemInt, no KemString. Python's own built-in types are the runtime types. This means all of Python's arithmetic, comparison, and string operations work natively on kemlang-py values - the interpreter just delegates to Python.
int42, 0, -7arbitrary precision, same as Python intfloat3.14, 0.5, -1.0IEEE 754 double, same as Python floatstr"hello", "123"UTF-8, double-quoted only, single-lineboolbhai chhe / bhai nathiPython True/False - subclass of intNone(no literal)result of failed input coercion; not user-writableDynamic typing
kemlang-py is dynamically typed. Variables have no declared type - they hold whatever value was assigned to them, and that type can change on reassignment.
kem bhai aa x che 42 # x is int x che "hello" # x is now str - perfectly legal x che bhai chhe # x is now bool aavjo bhai
The interpreter discovers the type of a value at runtime by calling Python's built-in isinstance(). It does not track types statically. Type errors are discovered when an incompatible operation is attempted - for example, trying to subtract a string from a number.
Truthiness
Conditions in jo and jya sudhi accept any KemValue, not just booleans. The interpreter converts the value to a boolean using the same rules as Python:
bhai chhe (True)yesthe canonical truthy valuebhai nathi (False)nothe canonical falsy valueany non-zero intyes0 is falsy; 1, -5, 42 are truthy0nozero integer is falsyany non-zero floatyes0.0 is falsy0.0nozero float is falsyany non-empty stryes"hello", "0", " " are all truthy"" (empty string)noempty string is falsyNonenoNone is always falsydef is_truthy(self, value: KemValue) -> bool:
if value is None: return False
if isinstance(value, bool): return value
if isinstance(value, int): return value != 0
if isinstance(value, float): return value != 0.0
if isinstance(value, str): return len(value) > 0
return FalseArithmetic and type rules
kemlang-py follows Python's numeric promotion rules for arithmetic: integer arithmetic stays integer; mixing int with float promotes to float.
int op int -> int (5 + 3 = 8, 7 / 2 = 3 integer division)
int op float -> float (5 + 3.0 = 8.0)
float op float -> float (1.5 * 2.0 = 3.0)
int op bool -> int (bool is a subclass of int in Python)
Division: 7 / 2 uses Python's / -> 3.5 (float, not integer 3)
Modulo: 7 % 3 -> 1 (remainder)
Negation: -5 -> int
-3.14 -> floatThe + operator: addition or concatenation
The + operator has two behaviours: numeric addition and string concatenation. If either operand is a string, both operands are converted to strings and concatenated. Otherwise, numeric addition is performed.
def evaluate_binary(self, expr: Binary) -> KemValue:
left = self.evaluate(expr.left)
right = self.evaluate(expr.right)
op = expr.operator.type
if op == TokenType.PLUS:
if isinstance(left, str) or isinstance(right, str):
return self.stringify(left) + self.stringify(right) # concatenate
return left + right # numeric addition 10 + 5 -> 15 both int, numeric addition
10 + 3.14 -> 13.14 int + float = float addition
"score: " + 10 -> "score: 10" str on left, stringify right
10 + " points" -> "10 points" str on right, stringify left
"a" + "b" -> "ab" both str, concatenate
stringify() rules:
int: str(value) -> "42"
float: str(value) -> "3.14"
bool: "bhai chhe" / "bhai nathi" (not Python's True/False)
None: "none"Comparison operators
Comparison operators (== != < > <= >=) return Python booleans (True /False), which kemlang-py treats as bhai chhe / bhai nathi.
Equality (==) compares value and type - Python's default == behaviour. The integer 1 equals the float 1.0 because Python promotes numeric types for comparison. The string "1" does not equal the integer 1 because they are different types.
10 == 10 -> bhai chhe (True) 10 == 10.0 -> bhai chhe (True - int/float promotion) 10 == "10" -> bhai nathi (False - different types) "a" < "b" -> bhai chhe (True - lexicographic) bhai chhe == 1 -> bhai chhe (True - bool is subclass of int) bhai nathi == 0 -> bhai chhe (True - False == 0 in Python)
Input coercion (bapu tame bolo)
bapu tame bolo always returns a string - it is Python's input() with the trailing newline stripped. When that string is used in arithmetic, the interpreter attempts to convert it:
# When doing arithmetic on a value that might be a string from input:
def coerce_to_number(self, value: KemValue) -> int | float:
if isinstance(value, (int, float)):
return value
if isinstance(value, str):
try:
return int(value) # try int first
except ValueError:
try:
return float(value) # then float
except ValueError:
raise RuntimeError(f"Cannot convert '{value}' to a number")This means bapu tame bolo works naturally for numeric input: if the user types 42, the string "42" is returned, and when it is used in arithmetic (e.g. x + 1), it is silently coerced to the integer 42. If the user types hello and you try to add a number to it, you get a RuntimeError.
Full execution lifecycle
Here is everything that happens from the moment you type kem run hello.jsk to when the process exits:
$ kem run hello.jsk
1. CLI (kemlang/cli.py)
- typer parses the command and file argument
- validates the .jsk extension (warns if different)
- reads the file with Path(file).read_text(encoding="utf-8")
2. Lexer (kemlang/lexer.py)
- Lexer(source).tokenize() called
- scans source string left-to-right
- returns List[Token] including EOF
- raises LexerError on bad character -> exit code 1
3. Parser (kemlang/parser.py)
- Parser(tokens).parse() called
- filters NEWLINE tokens from list
- checks for KEM_BHAI at position 0
- recursive descent builds Program dataclass tree
- raises ParseError on grammar violation -> exit code 1
4. Interpreter (kemlang/interpreter.py)
- Interpreter().interpret(program) called
- creates global Environment
- iterates program.statements, calling execute() on each
- each Print calls output_fn (Python's print) -> stdout
- each Input calls input_fn (Python's input) <- stdin
- RuntimeError caught at top level -> exit code 1
5. CLI (kemlang/cli.py)
- receives exit code from interpret()
- raise typer.Exit(exit_code)
- Python process exits with that code
$ echo $?
0 <- successError propagation
All three error types map to exit code 1. The distinction is only in the message:
LexerErrorRaised by the lexer when a character cannot start any valid token. Program exits immediately - no parsing or execution occurs.ParseErrorRaised by the parser when the token stream violates the grammar. Program exits immediately - no execution occurs.RuntimeErrorRaised by the interpreter during execution. Caught at the top of interpret(), which prints the message and returns exit code 1.