RFC 000: Incan Core Language RFC (Phase 1)¶
Status: Done
Created: 2024-11-26
This RFC consolidates the core semantics decisions for Incan's first implementation phase.
1. Core Semantics¶
1.1 Typing Model¶
- Required types: All bindings, parameters, and return types must have explicit types; no gradual or optional typing.
- Compile-time enforcement: Type mismatches fail at compile time, not runtime.
- Inference: Local type inference is allowed where unambiguous (e.g.,
x=42infersint), but function signatures require explicit types.
1.2 Bindings and Mutability¶
- Immutable by default: Bindings are immutable unless marked
mut. - Syntax:
x = valuefor immutable;mut x = valuefor mutable. - Optional
let:let x = valueis equivalent tox = value;letis optional for Pythonic feel.
1.3 Receivers (self / mut self)¶
selfis a keyword, not a convention; it must appear explicitly in method signatures.- Immutable receiver:
def method(self, ...)— read-only access to fields. - Mutable receiver:
def method(mut self, ...)— can mutate fields. - Static dispatch: Method calls resolve statically unless trait objects are used.
- No full borrow checker: Simplified mutability model (
selfvsmut self) without Rust's full ownership/borrowing.
1.4 Error Handling (Result / ?)¶
Result[T, E]: Primary error type; representsOk(value)orErr(error).Option[T]: RepresentsSome(value)orNone; for optional values.?operator: If expression isErr(e), early-return with that error; ifOk(v), unwrap tov.- Compile-time enforcement: Functions using
?must return a compatibleResult. - Panics are exceptional: Reserved for unrecoverable errors; normal errors use
Result. unwrap()/expect()are for unrecoverable paths only; normal errors useResult+?.- Optional sugar:
Result[T, E1 | E2]may be provided as syntax sugar, desugaring to a generated enum so?still operates on a single concrete error type. Prefer one error type per function; usemap_err/Fromconversions as needed.
1.5 Traits¶
- Behavior-only: Traits define methods only; no storage/fields. Traits must not declare fields; if a trait assumes fields exist, they must be provided by the adopter. This is enforced at compile time.
- Default methods: Traits can provide default implementations that may assume fields exist on the adopter.
- Deterministic composition: Multiple traits compose left-to-right; order is explicit and compile-time resolved.
- No Python MRO: No dynamic method resolution order; conflicts must be resolved explicitly.
- No runtime patching: Traits cannot be added at runtime; no monkey-patching.
- Conflict resolution: When multiple traits provide the same method, the adopting class must explicitly resolve (e.g.,
TraitA.method(self, ...)). - Optional
@requires: Annotation to declare expected fields/methods on adopters.
trait Loggable:
def log(self, msg: str):
print(f"[{self.name}] {msg}") # assumes self.name exists
# With explicit requires (optional ergonomic)
@requires(name: str)
trait Loggable:
def log(self, msg: str):
print(f"[{self.name}] {msg}")
1.6 Models¶
- Data-first containers: Declarative fields with optional validation.
- Own storage: Models own their fields.
- Validation: Via
@derive(Validate)or manualvalidate(self) -> Result[Self, E]methods. - Cannot inherit from classes: Models are not classes; they are data containers.
- Can implement traits: Models can adopt storage-free traits.
@derive(Validate)
model User:
id: UserId
email: Email
is_active: bool = true
def validate(self) -> Result[User, ValidationError]:
if not self.is_active and not self.email.as_str().endswith("@example.com"):
return Err(ValidationError.InactiveExternal)
return Ok(self)
1.7 Classes¶
- General-purpose types: Fields, methods, single inheritance, trait composition.
- Own storage: Classes own their fields.
- Single inheritance:
class Child extends Parent. - Trait composition:
class Service with TraitA, TraitB.
class Service with Loggable:
name: str
repo: UserRepo
def onboard(self, user: User) -> Result[User, ValidationError]:
self.log(f"creating {user.email.as_str()}")
return self.repo.save(user)
1.8 Newtypes¶
- Zero-cost wrappers: Enforce invariants at construction.
- Constructors return
Result: Validation failures are explicit. Newtypes must be constructed via their validated constructors; direct field construction is disallowed. Codegen enforces the constructor path. - Type-safe: Newtypes are distinct from their underlying type.
type Email = newtype str:
@validate new
def from_str(v: str) -> Result[Email, ValidationError]:
if "@" not in v:
return Err(ValidationError.EmailMissingAt)
return Ok(Email(v.lower()))
type UserId = newtype uuid
1.9 Enums¶
- Algebraic data types: Variants can carry data.
- Exhaustive matching:
matchmust cover all variants.
enum ValidationError:
EmailMissingAt
InactiveExternal
RepoError(str)
enum Option[T]:
Some(T)
None
enum Result[T, E]:
Ok(T)
Err(E)
1.10 Functions¶
defkeyword:def name(params) -> ReturnType:.async def: For async functions.- Explicit return types: Required for all functions.
- No implicit returns: Functions must use
return. (Future: implicit last-expression returns could be considered, but are not allowed now.)
def load_config(path: Path) -> Result[Config, IoError]:
raw = read_file(path)?
return Config.parse(raw)?
async def fetch_all(urls: List[str]) -> List[Bytes]:
tasks = [fetch(u) for u in urls]
return await gather(tasks)
2. Builtin Types and Traits¶
2.1 Core Types¶
| Type | Description |
|---|---|
int |
Signed integer (platform-sized or explicit: i32, i64) |
float |
Floating point (f32, f64) |
bool |
Boolean (true, false) |
str |
UTF-8 string |
bytes |
Byte sequence |
List[T] |
Growable list |
Dict[K, V] |
Hash map |
Set[T] |
Hash set |
Tuple[T, ...] |
Fixed-size heterogeneous tuple |
Option[T] |
Some(T) or None |
Result[T, E] |
Ok(T) or Err(E) |
Unit |
Void/unit type (empty tuple) |
2.2 Core Traits¶
| Trait | Description | Python Analog |
|---|---|---|
Debug |
Debug formatting | __repr__ |
Display |
User-facing formatting | __str__ |
Eq |
Equality comparison | __eq__ |
PartialEq |
Partial equality | __eq__ |
Ord |
Total ordering | __lt__, __le__, etc. |
PartialOrd |
Partial ordering | __lt__, __le__, etc. |
Hash |
Hashable | __hash__ |
Clone |
Explicit duplication | copy.copy |
Default |
Default construction | __init__ with defaults |
From[T] |
Conversion from T |
@classmethod constructor |
Into[T] |
Conversion into T |
— |
TryFrom[T] |
Fallible conversion from T |
— |
TryInto[T] |
Fallible conversion into T |
— |
Iterator |
Iteration protocol | __iter__, __next__ |
IntoIterator |
Convert to iterator | __iter__ |
Error |
Error behavior | Exception |
2.3 Derives¶
Derives auto-generate trait implementations for models and newtypes:
| Derive | Generated |
|---|---|
@derive(Debug) |
Debug formatting |
@derive(Display) |
Display formatting |
@derive(Eq) |
Equality + PartialEq |
@derive(Ord) |
Ordering + PartialOrd |
@derive(Hash) |
Hash implementation |
@derive(Clone) |
Clone implementation |
@derive(Default) |
Default construction |
@derive(Validate) |
Validation hooks |
Multiple derives: @derive(Debug, Eq, Hash, Clone).
3. Grammar (EBNF-like)¶
(* Top-level declarations *)
program = { declaration } ;
declaration = model_decl | class_decl | trait_decl | newtype_decl
| enum_decl | function_decl | import_decl ;
(* Imports *)
import_decl = import_stmt | from_import_stmt ;
import_stmt = "import" ( python_import | rust_import | module_path ) [ "as" IDENT ] ;
from_import_stmt
= "from" rust_import "import" import_item { "," import_item }
| "from" module_path "import" import_item { "," import_item } ;
import_item = IDENT [ "as" IDENT ] ;
python_import = "python" STRING ;
rust_import = "rust" "::" IDENT { "::" IDENT } ;
(* Module paths *)
module_path = parent_prefix IDENT { ( "::" | "." ) IDENT } ;
parent_prefix = { ".." }
| { "super" ( "::" | "." ) } ;
(*
Note: Absolute (project-root) module paths via `crate::...` are specified by RFC 005.
*)
(* Model *)
model_decl = { decorator } "model" IDENT [ type_params ] ":"
INDENT { field_decl | method_decl } DEDENT ;
field_decl = IDENT ":" type [ "=" expr ] ;
(* Class *)
class_decl = { decorator } "class" IDENT [ type_params ]
[ "extends" IDENT ] [ "with" trait_list ] ":"
INDENT { field_decl | method_decl } DEDENT ;
trait_list = IDENT { "," IDENT } ;
(* Trait *)
trait_decl = { decorator } "trait" IDENT [ type_params ] ":"
INDENT { method_decl } DEDENT ;
(* Newtype *)
newtype_decl = "type" IDENT "=" "newtype" type [ ":" INDENT { method_decl } DEDENT ] ;
(* Enum *)
enum_decl = "enum" IDENT [ type_params ] ":"
INDENT { variant_decl } DEDENT ;
variant_decl = IDENT [ "(" type_list ")" ] ;
(* Function / Method *)
function_decl = { decorator } [ "async" ] "def" IDENT "(" params ")" "->" type ":"
INDENT { statement } DEDENT ;
method_decl = { decorator } [ "async" ] "def" IDENT "(" receiver [ "," params ] ")" "->" type ":"
INDENT { statement } DEDENT
| { decorator } [ "async" ] "def" IDENT "(" params ")" "->" type ":" "..." ;
receiver = "self" | "mut" "self" ;
params = [ param { "," param } ] ;
param = IDENT ":" type [ "=" expr ] ;
(* Decorators *)
decorator = "@" IDENT [ "(" decorator_args ")" ] ;
decorator_args = expr { "," expr } | IDENT ":" type { "," IDENT ":" type } ;
(Note: Core supports both `import foo.bar` and `from foo import bar`. Decorator syntax is reserved; semantics are defined per decorator in later phases/stdlib.)
(* Types *)
type = simple_type | generic_type | function_type ;
simple_type = IDENT | "Unit" ;
generic_type = IDENT "[" type_list "]" ;
type_list = type { "," type } ;
type_params = "[" IDENT { "," IDENT } "]" ;
function_type = "(" type_list ")" "->" type ;
(* Expressions *)
expr = primary | binary_expr | unary_expr | call_expr | index_expr
| field_expr | method_expr | await_expr | try_expr | match_expr
| if_expr | list_comp | dict_comp | closure_expr ;
(* Note: Python-style lambda intentionally omitted in favor of arrow closures *)
closure_expr = "(" [ IDENT { "," IDENT } ] ")" "=>" expr ;
primary = IDENT | literal | "(" expr ")" | "self" ;
literal = INT | FLOAT | STRING | "true" | "false" | "None"
| list_literal | dict_literal | tuple_literal ;
list_literal = "[" [ expr { "," expr } ] "]" ;
dict_literal = "{" [ dict_entry { "," dict_entry } ] "}" ;
dict_entry = expr ":" expr ;
tuple_literal = "(" expr "," [ expr { "," expr } ] ")" ;
binary_expr = expr binary_op expr ;
binary_op = "+" | "-" | "*" | "/" | "%" | "==" | "!=" | "<" | ">" | "<=" | ">="
| "and" | "or" | "in" | "not in" ;
unary_expr = unary_op expr ;
unary_op = "-" | "not" ;
call_expr = expr "(" [ arg_list ] ")" ;
arg_list = arg { "," arg } ;
arg = expr | IDENT "=" expr ;
index_expr = expr "[" expr "]" ;
field_expr = expr "." IDENT ;
method_expr = expr "." IDENT "(" [ arg_list ] ")" ;
await_expr = "await" expr ;
try_expr = expr "?" ;
match_expr = "match" expr ":" INDENT { match_arm } DEDENT ;
match_arm = "case" pattern ":" INDENT { statement } DEDENT
| pattern "=>" expr
| pattern "=>" INDENT { statement } DEDENT ;
pattern = IDENT | literal | IDENT "(" pattern_list ")" | "_" ;
pattern_list = pattern { "," pattern } ;
if_expr = "if" expr ":" INDENT { statement } DEDENT
{ "elif" expr ":" INDENT { statement } DEDENT }
[ "else" ":" INDENT { statement } DEDENT ] ;
list_comp = "[" expr "for" IDENT "in" expr [ "if" expr ] "]" ;
dict_comp = "{" expr ":" expr "for" IDENT "in" expr [ "if" expr ] "}" ;
(* Statements *)
statement = assignment | return_stmt | if_stmt | while_stmt | for_stmt
| expr_stmt | pass_stmt ;
assignment = [ "let" | "mut" ] IDENT [ ":" type ] "=" expr ;
return_stmt = "return" [ expr ] ;
if_stmt = if_expr ;
while_stmt = "while" expr ":" INDENT { statement } DEDENT ;
for_stmt = "for" IDENT "in" expr ":" INDENT { statement } DEDENT ;
expr_stmt = expr ;
pass_stmt = "pass" | "..." ;
4. Type Rules¶
4.1 Assignment¶
- LHS type must match RHS type exactly (or be inferred).
mutbindings can be reassigned; non-mutbindings cannot.
4.2 Function Signatures¶
- All parameters must have explicit types.
- Return type must be explicit.
- Functions using
?must returnResult[T, E]whereEis compatible with the propagated error.
4.3 Method Resolution¶
- Check the type's own methods.
- Check composed traits in left-to-right order.
- If conflict, require explicit resolution or compile error.
4.4 Trait Adoption¶
class X with TraitA, TraitB:Xmust satisfy all trait requirements.- Missing required methods → compile error.
- Conflicting methods → must resolve explicitly.
4.5 Error Propagation (?)¶
expr?onResult[T, E]:- If
Err(e): returnErr(e)from enclosing function. - If
Ok(v): evaluate tov.
- If
- Enclosing function must return
Result[_, E](or compatible).
4.6 Newtype Invariants¶
- Newtype constructors returning
Resultenforce invariants. - Cannot construct newtype directly; must use constructor.
5. Invariants¶
- No implicit mutation: All mutations require
mut. - No silent errors: Errors surface via
Result;?propagates explicitly. - No runtime trait patching: Trait composition is static.
- Deterministic dispatch: Method resolution is predictable (no MRO).
- Type safety: All types known at compile time; no dynamic typing.
- Explicit receiver:
selfis required in method signatures.
6. Examples¶
Complete Example¶
import polars::prelude as pl
# Structured error type
enum AppError:
ValidationError(str)
IoError(str)
NotFound
# Newtype with invariant
type Email = newtype str:
@validate new
def from_str(v: str) -> Result[Email, AppError]:
if "@" not in v:
return Err(AppError.ValidationError("missing @"))
return Ok(Email(v.lower()))
def as_str(self) -> str:
return self.0 # access underlying value
# Model with validation
@derive(Debug, Eq, Clone)
model User:
id: int
email: Email
is_active: bool = true
def validate(self) -> Result[User, AppError]:
if not self.is_active:
return Err(AppError.ValidationError("inactive"))
return Ok(self)
# Trait
trait Loggable:
def log(self, msg: str):
print(f"[{self.name}] {msg}")
# Class with trait
class UserService with Loggable:
name: str
users: Dict[int, User]
def create(mut self, email_str: str) -> Result[User, AppError]:
email = Email.from_str(email_str)?
user = User(id=len(self.users) + 1, email=email).validate()?
self.users[user.id] = user
self.log(f"created user {user.id}")
return Ok(user)
def get(self, id: int) -> Result[User, AppError]:
match self.users.get(id):
case Some(user): Ok(user)
case None: Err(AppError.NotFound)
# Usage
def main() -> Result[Unit, AppError]:
mut svc = UserService(name="user-svc", users={})
user = svc.create("ada@example.com")?
print(f"Created: {user}")
return Ok(())
7. Checklist¶
Core Semantics¶
- [x] Required typing
- [x] Immutable-by-default bindings
- [x]
self/mut selfreceivers - [x]
Result/Option/?error handling - [x] Behavior-only traits with deterministic composition
- [x] Models vs Classes distinction
- [x] Newtypes with validation
- [x] Enums (algebraic data types)
Builtin Types¶
- [x] Primitives:
int,float,bool,str,bytes - [x] Collections:
List,Dict,Set,Tuple - [x] Error types:
Result,Option - [x]
Unittype
Builtin Traits¶
- [x]
Debug,Display - [x]
Eq,PartialEq,Ord,PartialOrd - [x]
Hash - [x]
Clone - [x]
Default - [x]
From,Into,TryFrom,TryInto - [x]
Iterator,IntoIterator - [x]
Error
Derives¶
- [x]
@derive(Debug, Display, Eq, Ord, Hash, Clone, Default, Validate)
Grammar¶
- [x] Declarations: model, class, trait, newtype, enum, function
- [x] Expressions: calls,
?, match, if, comprehensions - [x] Statements: assignment, return, control flow
- [x] Receivers:
self,mut self
Type Rules¶
- [x] Assignment typing
- [x] Function signature requirements
- [x] Method resolution order
- [x] Trait adoption rules
- [x] Error propagation (
?) - [x] Newtype invariants