docs

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:

kemlang/types.py
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.

Python typekemlang-py literalnotes
int42, 0, -7arbitrary precision, same as Python int
float3.14, 0.5, -1.0IEEE 754 double, same as Python float
str"hello", "123"UTF-8, double-quoted only, single-line
boolbhai chhe / bhai nathiPython True/False - subclass of int
None(no literal)result of failed input coercion; not user-writable

Dynamic 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.

valid in kemlang-py: x changes type 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:

valuetruthy?notes
bhai chhe (True)yesthe canonical truthy value
bhai nathi (False)nothe canonical falsy value
any non-zero intyes0 is falsy; 1, -5, 42 are truthy
0nozero integer is falsy
any non-zero floatyes0.0 is falsy
0.0nozero float is falsy
any non-empty stryes"hello", "0", " " are all truthy
"" (empty string)noempty string is falsy
NonenoNone is always falsy
kemlang/interpreter.py - is_truthy
def 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 False

Arithmetic and type rules

kemlang-py follows Python's numeric promotion rules for arithmetic: integer arithmetic stays integer; mixing int with float promotes to float.

arithmetic type rules
  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  ->  float

The + 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.

kemlang/interpreter.py - evaluate_binary (+ operator)
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
+ coercion examples
  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.

comparison examples
  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:

kemlang/interpreter.py - numeric coercion in arithmetic
# 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:

complete execution lifecycle
  $ 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    <- success

Error propagation

All three error types map to exit code 1. The distinction is only in the message:

error typewhen it is raised
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.