Files
centvrion/centvrion/compiler/emit_expr.py
2026-04-25 18:34:47 +02:00

479 lines
17 KiB
Python

from centvrion.errors import CentvrionError
from centvrion.ast_nodes import (
String, InterpolatedString, Numeral, Fractio, Bool, Nullus, ID,
BinOp, UnaryMinus, UnaryNot,
ArrayIndex, ArraySlice, DataArray, DataRangeArray, DataDict,
BuiltIn, Invoca, Fvnctio,
num_to_int, frac_to_fraction,
)
_BINOP_FN = {
"SYMBOL_PLUS": "cent_add",
"SYMBOL_MINUS": "cent_sub",
"SYMBOL_TIMES": "cent_mul",
"SYMBOL_DIVIDE": "cent_div",
"SYMBOL_AMPERSAND": "cent_concat",
"SYMBOL_AT": "cent_array_concat",
"KEYWORD_RELIQVVM": "cent_mod",
"KEYWORD_EST": "cent_eq",
"KEYWORD_DISPAR": "cent_neq",
"KEYWORD_MINVS": "cent_lt",
"KEYWORD_PLVS": "cent_gt",
"KEYWORD_HAVD_PLVS": "cent_lte",
"KEYWORD_HAVD_MINVS": "cent_gte",
"KEYWORD_ET": "cent_and",
"KEYWORD_AVT": "cent_or",
}
def _escape(s):
return (s
.replace("\\", "\\\\")
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t"))
def emit_expr(node, ctx):
"""
Emit C code for a CENTVRION expression node.
Returns (lines, result_var):
lines — list of C statements that compute the expression
result_var — name of the CentValue variable holding the result
"""
if isinstance(node, Numeral):
tmp = ctx.fresh_tmp()
magnvm = "MAGNVM" in ctx.modules
svbnvlla = "SVBNVLLA" in ctx.modules
n = num_to_int(node.value, magnvm, svbnvlla)
return [f"CentValue {tmp} = cent_int({n}L);"], tmp
if isinstance(node, String):
tmp = ctx.fresh_tmp()
return [f'CentValue {tmp} = cent_str("{_escape(node.value)}");'], tmp
if isinstance(node, InterpolatedString):
if len(node.parts) == 0:
tmp = ctx.fresh_tmp()
return [f'CentValue {tmp} = cent_str("");'], tmp
if len(node.parts) == 1:
return emit_expr(node.parts[0], ctx)
l_lines, l_var = emit_expr(node.parts[0], ctx)
r_lines, r_var = emit_expr(node.parts[1], ctx)
lines = l_lines + r_lines
acc = ctx.fresh_tmp()
lines.append(f"CentValue {acc} = cent_concat({l_var}, {r_var});")
for part in node.parts[2:]:
p_lines, p_var = emit_expr(part, ctx)
lines.extend(p_lines)
new_acc = ctx.fresh_tmp()
lines.append(f"CentValue {new_acc} = cent_concat({acc}, {p_var});")
acc = new_acc
return lines, acc
if isinstance(node, Bool):
tmp = ctx.fresh_tmp()
v = "1" if node.value else "0"
return [f"CentValue {tmp} = cent_bool({v});"], tmp
if isinstance(node, Nullus):
tmp = ctx.fresh_tmp()
return [f"CentValue {tmp} = cent_null();"], tmp
if isinstance(node, Fractio):
if not ctx.has_module("FRACTIO"):
raise CentvrionError("Cannot use fraction literals without 'FRACTIO' module")
tmp = ctx.fresh_tmp()
magnvm = "MAGNVM" in ctx.modules
svbnvlla = "SVBNVLLA" in ctx.modules
frac = frac_to_fraction(node.value, magnvm, svbnvlla)
return [f"CentValue {tmp} = cent_frac({frac.numerator}L, {frac.denominator}L);"], tmp
if isinstance(node, ID):
tmp = ctx.fresh_tmp()
return [f'CentValue {tmp} = cent_scope_get(&_scope, "{node.name}");'], tmp
if isinstance(node, BinOp):
# Short-circuit for logical operators
if node.op in ("KEYWORD_AVT", "KEYWORD_ET"):
l_lines, l_var = emit_expr(node.left, ctx)
r_lines, r_var = emit_expr(node.right, ctx)
tmp = ctx.fresh_tmp()
if node.op == "KEYWORD_AVT":
lines = l_lines + [f"CentValue {tmp};"]
lines += [f"if (cent_truthy({l_var})) {{ {tmp} = cent_bool(1); }} else {{"]
lines += [f" {l}" for l in r_lines]
lines += [f" {tmp} = cent_bool(cent_truthy({r_var}));"]
lines += ["}"]
else:
lines = l_lines + [f"CentValue {tmp};"]
lines += [f"if (!cent_truthy({l_var})) {{ {tmp} = cent_bool(0); }} else {{"]
lines += [f" {l}" for l in r_lines]
lines += [f" {tmp} = cent_bool(cent_truthy({r_var}));"]
lines += ["}"]
return lines, tmp
l_lines, l_var = emit_expr(node.left, ctx)
r_lines, r_var = emit_expr(node.right, ctx)
tmp = ctx.fresh_tmp()
if node.op == "SYMBOL_DIVIDE" and ctx.has_module("FRACTIO"):
fn = "cent_div_frac"
elif node.op == "KEYWORD_RELIQVVM" and ctx.has_module("FRACTIO"):
fn = "cent_mod_frac"
else:
fn = _BINOP_FN[node.op]
return l_lines + r_lines + [f"CentValue {tmp} = {fn}({l_var}, {r_var});"], tmp
if isinstance(node, UnaryMinus):
inner_lines, inner_var = emit_expr(node.expr, ctx)
tmp = ctx.fresh_tmp()
return inner_lines + [f"CentValue {tmp} = cent_neg({inner_var});"], tmp
if isinstance(node, UnaryNot):
inner_lines, inner_var = emit_expr(node.expr, ctx)
tmp = ctx.fresh_tmp()
return inner_lines + [f"CentValue {tmp} = cent_bool(!cent_truthy({inner_var}));"], tmp
if isinstance(node, ArrayIndex):
arr_lines, arr_var = emit_expr(node.array, ctx)
idx_lines, idx_var = emit_expr(node.index, ctx)
tmp = ctx.fresh_tmp()
return arr_lines + idx_lines + [f"CentValue {tmp} = cent_list_index({arr_var}, {idx_var});"], tmp
if isinstance(node, ArraySlice):
arr_lines, arr_var = emit_expr(node.array, ctx)
lo_lines, lo_var = emit_expr(node.from_index, ctx)
hi_lines, hi_var = emit_expr(node.to_index, ctx)
tmp = ctx.fresh_tmp()
return arr_lines + lo_lines + hi_lines + [
f"CentValue {tmp} = cent_list_slice({arr_var}, {lo_var}, {hi_var});"
], tmp
if isinstance(node, DataArray):
lines = []
tmp = ctx.fresh_tmp()
lines.append(f"CentValue {tmp} = cent_list_new({len(node.content)});")
for item in node.content:
item_lines, item_var = emit_expr(item, ctx)
lines.extend(item_lines)
lines.append(f"cent_list_push(&{tmp}, {item_var});")
return lines, tmp
if isinstance(node, DataRangeArray):
lo_lines, lo_var = emit_expr(node.from_value, ctx)
hi_lines, hi_var = emit_expr(node.to_value, ctx)
tmp = ctx.fresh_tmp()
i_var = ctx.fresh_tmp()
if node.step is None:
cap = f"({hi_var}.ival >= {lo_var}.ival ? (int)({hi_var}.ival - {lo_var}.ival + 1) : 0)"
lines = lo_lines + hi_lines + [
f"CentValue {tmp} = cent_list_new({cap});",
f"for (long {i_var} = {lo_var}.ival; {i_var} <= {hi_var}.ival; {i_var}++) {{",
f" cent_list_push(&{tmp}, cent_int({i_var}));",
"}",
]
return lines, tmp
step_lines, step_var = emit_expr(node.step, ctx)
lines = lo_lines + hi_lines + step_lines + [
f'if ({step_var}.type != CENT_INT) cent_type_error("Range step must be a number");',
f'if ({step_var}.ival == 0) cent_runtime_error("Range step cannot be zero");',
f"CentValue {tmp} = cent_list_new(0);",
f"for (long {i_var} = {lo_var}.ival; ({step_var}.ival > 0 ? {i_var} <= {hi_var}.ival : {i_var} >= {hi_var}.ival); {i_var} += {step_var}.ival) {{",
f" cent_list_push(&{tmp}, cent_int({i_var}));",
"}",
]
return lines, tmp
if isinstance(node, DataDict):
lines = []
tmp = ctx.fresh_tmp()
lines.append(f"CentValue {tmp} = cent_dict_new({len(node.pairs)});")
for key_node, val_node in node.pairs:
k_lines, k_var = emit_expr(key_node, ctx)
v_lines, v_var = emit_expr(val_node, ctx)
lines.extend(k_lines)
lines.extend(v_lines)
lines.append(f"cent_dict_set(&{tmp}, {k_var}, {v_var});")
return lines, tmp
if isinstance(node, BuiltIn):
return _emit_builtin(node, ctx)
if isinstance(node, Invoca):
return _emit_invoca(node, ctx)
if isinstance(node, Fvnctio):
return _emit_fvnctio(node, ctx)
raise NotImplementedError(type(node).__name__)
def _emit_builtin(node, ctx):
lines = []
param_vars = []
for p in node.parameters:
p_lines, p_var = emit_expr(p, ctx)
lines.extend(p_lines)
param_vars.append(p_var)
tmp = ctx.fresh_tmp()
match node.builtin:
case "DIC":
if not param_vars:
lines.append('cent_dic(cent_str(""));')
lines.append(f'CentValue {tmp} = cent_str("");')
elif len(param_vars) == 1:
lines.append(f"cent_dic({param_vars[0]});")
lines.append(f"CentValue {tmp} = {param_vars[0]};")
else:
acc = param_vars[0]
for pv in param_vars[1:]:
space_tmp = ctx.fresh_tmp()
joined_tmp = ctx.fresh_tmp()
lines.append(f'CentValue {space_tmp} = cent_concat({acc}, cent_str(" "));')
lines.append(f"CentValue {joined_tmp} = cent_concat({space_tmp}, {pv});")
acc = joined_tmp
lines.append(f"cent_dic({acc});")
lines.append(f"CentValue {tmp} = {acc};")
case "AVDI":
lines.append(f"CentValue {tmp} = cent_avdi();")
case "AVDI_NVMERVS":
lines.append(f"CentValue {tmp} = cent_avdi_numerus();")
case "LONGITVDO":
lines.append(f"CentValue {tmp} = cent_longitudo({param_vars[0]});")
case "LITTERA":
lines.append(f"CentValue {tmp} = cent_littera({param_vars[0]});")
case "MAIVSCVLA":
if len(param_vars) != 1:
lines.append(f'cent_runtime_error("MAIVSCVLA takes exactly I argument");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_maivscvla({param_vars[0]});")
case "MINVSCVLA":
if len(param_vars) != 1:
lines.append(f'cent_runtime_error("MINVSCVLA takes exactly I argument");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_minvscvla({param_vars[0]});")
case "FORTVITVS_NVMERVS":
if not ctx.has_module("FORS"):
lines.append('cent_runtime_error("FORS module required for FORTVITVS_NVMERVS");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_fortuitus_numerus({param_vars[0]}, {param_vars[1]});")
case "FORTVITA_ELECTIO":
if not ctx.has_module("FORS"):
lines.append('cent_runtime_error("FORS module required for FORTVITA_ELECTIO");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_fortuita_electionis({param_vars[0]});")
case "DECIMATIO":
if not ctx.has_module("FORS"):
lines.append('cent_runtime_error("FORS module required for DECIMATIO");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_decimatio({param_vars[0]});")
case "SEMEN":
if not ctx.has_module("FORS"):
lines.append('cent_runtime_error("FORS module required for SEMEN");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_semen({param_vars[0]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "SENATVS":
if param_vars:
arr_tmp = ctx.fresh_tmp() + "_arr"
lines.append(f"CentValue {arr_tmp}[] = {{{', '.join(param_vars)}}};")
lines.append(f"CentValue {tmp} = cent_senatus({arr_tmp}, {len(param_vars)});")
else:
lines.append(f"CentValue {tmp} = cent_senatus(NULL, 0);")
case "ERVMPE":
# break as expression (side-effecting; result is unused)
lines.append("break;")
lines.append(f"CentValue {tmp} = cent_null();")
case "CLAVES":
lines.append(f"CentValue {tmp} = cent_dict_keys({param_vars[0]});")
case "ORDINA":
lines.append(f"CentValue {tmp} = cent_ordina({param_vars[0]});")
case "ADDE":
lines.append(f"CentValue {tmp} = cent_adde({param_vars[0]}, {param_vars[1]});")
case "TOLLE":
lines.append(f"CentValue {tmp} = cent_tolle({param_vars[0]}, {param_vars[1]});")
case "INSERE":
lines.append(f"CentValue {tmp} = cent_insere({param_vars[0]}, {param_vars[1]}, {param_vars[2]});")
case "NECTE":
lines.append(f"CentValue {tmp} = cent_necte({param_vars[0]}, {param_vars[1]});")
case "IVNGE":
lines.append(f"CentValue {tmp} = cent_ivnge({param_vars[0]}, {param_vars[1]});")
case "EVERRE":
lines.append("cent_everre();")
lines.append(f"CentValue {tmp} = cent_null();")
case "TYPVS":
lines.append(f"CentValue {tmp} = cent_typvs({param_vars[0]});")
case "DORMI":
lines.append(f"cent_dormi({param_vars[0]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "LEGE":
if not ctx.has_module("SCRIPTA"):
lines.append('cent_runtime_error("SCRIPTA module required for LEGE");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_lege({param_vars[0]});")
case "SCRIBE":
if not ctx.has_module("SCRIPTA"):
lines.append('cent_runtime_error("SCRIPTA module required for SCRIBE");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_scribe({param_vars[0]}, {param_vars[1]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "ADIVNGE":
if not ctx.has_module("SCRIPTA"):
lines.append('cent_runtime_error("SCRIPTA module required for ADIVNGE");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_adivnge({param_vars[0]}, {param_vars[1]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "NVMERVS":
if len(param_vars) != 1:
lines.append(f'cent_runtime_error("NVMERVS takes exactly I argument");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_numerus({param_vars[0]});")
case "QVAERE":
lines.append(f"CentValue {tmp} = cent_qvaere({param_vars[0]}, {param_vars[1]});")
case "SVBSTITVE":
lines.append(f"CentValue {tmp} = cent_svbstitve({param_vars[0]}, {param_vars[1]}, {param_vars[2]});")
case "SCINDE":
lines.append(f"CentValue {tmp} = cent_scinde({param_vars[0]}, {param_vars[1]});")
case "PETE":
if not ctx.has_module("RETE"):
lines.append('cent_runtime_error("RETE module required for PETE");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_pete({param_vars[0]});")
case "PETITVR":
if not ctx.has_module("RETE"):
lines.append('cent_runtime_error("RETE module required for PETITVR");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_petitvr({param_vars[0]}, {param_vars[1]}, _scope);")
lines.append(f"CentValue {tmp} = cent_null();")
case "AVSCVLTA":
if not ctx.has_module("RETE"):
lines.append('cent_runtime_error("RETE module required for AVSCVLTA");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_avscvlta({param_vars[0]});")
lines.append(f"CentValue {tmp} = cent_null();")
case _:
raise NotImplementedError(node.builtin)
return lines, tmp
def _emit_invoca(node, ctx):
"""
Emits a user-defined function call.
Supports both static resolution (ID callee with known function) and
dynamic dispatch (arbitrary expression callee via CENT_FUNC values).
"""
lines = []
param_vars = []
for p in node.parameters:
p_lines, p_var = emit_expr(p, ctx)
lines.extend(p_lines)
param_vars.append(p_var)
# Try static resolution for simple ID callees
if isinstance(node.callee, ID):
c_func_name = ctx.func_resolve.get(node.callee.name)
if c_func_name is not None:
call_scope_var = ctx.fresh_tmp() + "_sc"
lines.append(f"CentScope {call_scope_var} = cent_scope_copy(&_scope);")
param_names = ctx.functions[c_func_name]
if len(param_vars) != len(param_names):
raise CentvrionError(
f"Function '{node.callee.name}' expects {len(param_names)} argument(s), "
f"got {len(param_vars)}"
)
for i, pname in enumerate(param_names):
lines.append(f'cent_scope_set(&{call_scope_var}, "{pname}", {param_vars[i]});')
tmp = ctx.fresh_tmp()
lines.append(f"CentValue {tmp} = {c_func_name}({call_scope_var});")
return lines, tmp
# Dynamic dispatch: evaluate callee, call via function pointer
callee_lines, callee_var = emit_expr(node.callee, ctx)
lines.extend(callee_lines)
lines.append(f'if ({callee_var}.type != CENT_FUNC) cent_type_error("cannot call non-function");')
call_scope_var = ctx.fresh_tmp() + "_sc"
lines.append(f"CentScope {call_scope_var} = cent_scope_copy(&_scope);")
nargs = len(param_vars)
lines.append(
f"if ({callee_var}.fnval.param_count != {nargs}) "
f'cent_runtime_error("wrong number of arguments");'
)
for i, pv in enumerate(param_vars):
lines.append(
f'cent_scope_set(&{call_scope_var}, '
f'{callee_var}.fnval.param_names[{i}], {pv});'
)
tmp = ctx.fresh_tmp()
lines.append(f"CentValue {tmp} = {callee_var}.fnval.fn({call_scope_var});")
return lines, tmp
def _emit_fvnctio(node, ctx):
"""Emit a FVNCTIO lambda expression as a CENT_FUNC value."""
c_name = ctx.lambda_names[id(node)]
param_names = ctx.functions[c_name]
tmp = ctx.fresh_tmp()
lines = []
# Build static param name array
params_arr = ctx.fresh_tmp() + "_pn"
lines.append(
f"static const char *{params_arr}[] = {{"
+ ", ".join(f'"{p}"' for p in param_names)
+ "};"
)
lines.append(
f"CentValue {tmp} = cent_func_val({c_name}, {params_arr}, {len(param_names)});"
)
return lines, tmp