codegen: type-driven dispatch for + between Int idents
Closes the known limitation from the self-host commit: `fn add(a:Int,
b:Int) { a + b }` now compiles to integer addition, not string concat.
Previously the codegen heuristic guessed string concat whenever both
operands were Idents with no literal anchor.
Mechanism
- parser captures the leading type identifier from `let x: T = ...`
bindings (new "type" field on Let) and from function parameter
annotations (new "type" field on each param).
- codegen maintains a per-function int-name set in process state via
state_set("__int_names", csv). cg_fn seeds it from typed parameters;
cg_stmt extends it from typed `let` bindings and from `let x = <Int
literal>` (literal inference).
- BinOp Plus: when both sides are Idents and both names are in the
int-name set, emit arithmetic; otherwise the existing literal-anchor
heuristic applies, with string concat as the fallback.
This is the first compiler change made entirely through the self-
hosting workflow — no Python bootstrap. Edit el source, run existing
elc on elc-combined.el, cc the output, test. Closure holds at the
new binary.
Tests
- add(40, 2) → 42
- count_to(10) → 45 (let i: Int / let total: Int rebinding)
- Regression suite (tiny/implret/whiletest/lextest) unchanged.
dist/platform/elc updated; .prev preserved.
This commit is contained in:
@@ -170,6 +170,23 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
let op_c: String = binop_to_c(op)
|
||||
return "(" + left_c + " " + op_c + " " + right_c + ")"
|
||||
}
|
||||
// Type-driven dispatch: if both sides are Idents declared
|
||||
// with type Int (parameters annotated `: Int` or let bindings
|
||||
// annotated `: Int`), this is arithmetic, not concat. The
|
||||
// current-function int-name set is maintained by cg_fn /
|
||||
// cg_stmt via state_set("__int_names", csv).
|
||||
if left_kind == "Ident" {
|
||||
if right_kind == "Ident" {
|
||||
let lname: String = left["name"]
|
||||
let rname: String = right["name"]
|
||||
if is_int_name(lname) {
|
||||
if is_int_name(rname) {
|
||||
let op_c: String = binop_to_c(op)
|
||||
return "(" + left_c + " " + op_c + " " + right_c + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if left_kind == "Call" {
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
@@ -188,8 +205,8 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
}
|
||||
// Ident + Ident or Ident + unknown — assume string concat
|
||||
// (This is the ambiguous case: El uses + for both string and integer ops)
|
||||
// Ident + Ident or Ident + unknown without int-typed evidence —
|
||||
// fall back to string concat (the historical heuristic).
|
||||
if left_kind == "Ident" {
|
||||
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
||||
}
|
||||
@@ -413,6 +430,17 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
let name: String = stmt["name"]
|
||||
let val = stmt["value"]
|
||||
let val_c: String = cg_expr(val)
|
||||
// If the binding is annotated `: Int` and val is an Int literal,
|
||||
// register `name` in the per-function int-name set so that later
|
||||
// `name + ...` dispatches to arithmetic, not concat.
|
||||
let ltype: String = stmt["type"]
|
||||
if str_eq(ltype, "Int") {
|
||||
add_int_name(name)
|
||||
}
|
||||
let vk: String = val["expr"]
|
||||
if str_eq(vk, "Int") {
|
||||
add_int_name(name)
|
||||
}
|
||||
if list_contains(declared, name) {
|
||||
emit_line(indent + name + " = " + val_c + ";")
|
||||
return declared
|
||||
@@ -627,6 +655,41 @@ fn transform_implicit_return(body: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
body
|
||||
}
|
||||
|
||||
// Test whether `name` is currently registered as an Int-typed identifier
|
||||
// for the function being codegened. The set is maintained as a comma-
|
||||
// bounded CSV in process state; cg_fn seeds it from typed parameters,
|
||||
// cg_stmt extends it from typed `let` bindings.
|
||||
fn is_int_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__int_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
fn add_int_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__int_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__int_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
|
||||
state_set("__int_names", ",")
|
||||
let np: Int = native_list_len(params)
|
||||
let pi = 0
|
||||
while pi < np {
|
||||
let param = native_list_get(params, pi)
|
||||
let pname: String = param["name"]
|
||||
let ptype: String = param["type"]
|
||||
if str_eq(ptype, "Int") {
|
||||
add_int_name(pname)
|
||||
}
|
||||
let pi = pi + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn cg_fn(stmt: Map<String, Any>) -> Void {
|
||||
let fn_name: String = stmt["name"]
|
||||
// Skip El's `fn main()` — C provides its own main() for top-level stmts
|
||||
@@ -636,6 +699,9 @@ fn cg_fn(stmt: Map<String, Any>) -> Void {
|
||||
let body = stmt["body"]
|
||||
let ret_type: String = stmt["ret_type"]
|
||||
let params_c: String = params_to_c(params)
|
||||
// Seed the per-function int-name set so the `+` codegen can dispatch
|
||||
// arithmetic vs concat on type-annotated identifiers.
|
||||
build_int_names_for_params(params)
|
||||
emit_line("el_val_t " + fn_name + "(" + params_c + ") {")
|
||||
// Seed declared with parameter names so reassignment works
|
||||
let decl = native_list_empty()
|
||||
|
||||
@@ -119,8 +119,15 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let pname = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let p = expect(tokens, p, "Colon")
|
||||
// Capture the leading type identifier so codegen can dispatch
|
||||
// arithmetic vs string-concat on `+` based on declared types.
|
||||
let ptype = ""
|
||||
let kt = tok_kind(tokens, p)
|
||||
if kt == "Ident" {
|
||||
let ptype = tok_value(tokens, p)
|
||||
}
|
||||
let p = skip_type(tokens, p)
|
||||
let param = { "name": pname }
|
||||
let param = { "name": pname, "type": ptype }
|
||||
let params = native_list_append(params, param)
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if k2 == "Comma" {
|
||||
@@ -533,17 +540,24 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let p = pos + 1
|
||||
let name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let ltype = ""
|
||||
let k2 = tok_kind(tokens, p)
|
||||
// optional type annotation: name: Type
|
||||
// optional type annotation: name: Type — capture the leading
|
||||
// identifier so codegen can dispatch arithmetic vs concat on
|
||||
// `+` between two typed Idents.
|
||||
if k2 == "Colon" {
|
||||
let p = p + 1
|
||||
let kt = tok_kind(tokens, p)
|
||||
if kt == "Ident" {
|
||||
let ltype = tok_value(tokens, p)
|
||||
}
|
||||
let p = skip_type(tokens, p)
|
||||
}
|
||||
let p = expect(tokens, p, "Eq")
|
||||
let r = parse_expr(tokens, p)
|
||||
let val = r["node"]
|
||||
let p = r["pos"]
|
||||
return make_result({ "stmt": "Let", "name": name, "value": val }, p)
|
||||
return make_result({ "stmt": "Let", "name": name, "value": val, "type": ltype }, p)
|
||||
}
|
||||
|
||||
// return statement
|
||||
|
||||
Reference in New Issue
Block a user