From 3ac0d8d5cbc3a07b9b818b641e23b7bc802a3be6 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Wed, 23 Sep 2026 16:02:02 +0300 Subject: [PATCH] feat(lang): numbers float - a module whose decimal literals are float Co-Authored-By: Claude Opus 5.5 --- LANGUAGE.md | 3 +++ changes/numbers-float.md | 9 +++++++++ selfhost/backend/emit_call.ludic | 5 +++++ selfhost/backend/emit_core.ludic | 1 + selfhost/frontend/parse.ludic | 18 ++++++++++++++++++ 5 files changed, 36 insertions(+) create mode 100644 changes/numbers-float.md diff --git a/LANGUAGE.md b/LANGUAGE.md index 427c4e8b..095442db 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -240,6 +240,9 @@ program Shade { exactly that decimal, not its Q16.16 approximation. With no float in sight it stays `fixed`, so existing code keeps its meaning. A whole literal expression (`1.0 / 3.0`) is evaluated in the context's type. +- **`numbers float`.** A file that begins with `numbers float` (or has it inside its `program` + block) takes bare decimal literals as `float`, not `fixed`; the files it imports inherit the + mode (runtime files never do). One line in a package's barrel makes the package float. - **Promotion.** `int` and `long` promote to the float type of the other operand; `float` with `double` promotes to `double`. - **Explicit conversions.** `float(x)`, `double(x)`, `int(x)` (truncates toward diff --git a/changes/numbers-float.md b/changes/numbers-float.md new file mode 100644 index 00000000..1c6ac6e3 --- /dev/null +++ b/changes/numbers-float.md @@ -0,0 +1,9 @@ +bump: minor +type: feat +**`numbers float` — a module whose decimal literals are `float`.** A bare `1.5` is `fixed` +unless a float is in sight, which is the right default for deterministic code and the wrong +one for a renderer or a game whose numbers are all IEEE: `Math.sqrt(2.0)` came out in `fixed`, +and every literal needed a `float(...)` round it. A file that says `numbers float` (at the top, +or inside a `program` block) gets float literals, and passes the mode on to every non-runtime +file it imports, so one line in a barrel or an entry file covers a whole package. Runtime files +under `runtime/` are never switched, so the engine's own fixed-point code keeps its meaning. diff --git a/selfhost/backend/emit_call.ludic b/selfhost/backend/emit_call.ludic index 87f953e6..abaffc2c 100644 --- a/selfhost/backend/emit_call.ludic +++ b/selfhost/backend/emit_call.ludic @@ -1437,6 +1437,11 @@ function emit_call(e: Node) -> Val { function emit_expr(e: Node) -> Val { if (e == null) { return val("0", "int") } if e.kind == E_INT { return val(itoa(e.ival), "int") } + if e.kind == E_FLOAT and is_float_file(e.file) { + let ff = val(fp_lit_code(e.s, "float"), "float") + ff.lit = e + return ff + } if e.kind == E_FLOAT { let fv = val(itoa(e.ival), "fixed"); fv.lit = e; return fv } if e.kind == E_PREVAL { return g_prevals[e.ival] } if e.kind == E_BOOL { return val(itoa(e.ival), "bool") } diff --git a/selfhost/backend/emit_core.ludic b/selfhost/backend/emit_core.ludic index ddaeb98c..c0fbd5ed 100644 --- a/selfhost/backend/emit_core.ludic +++ b/selfhost/backend/emit_core.ludic @@ -358,6 +358,7 @@ function static_type(e: Node) -> pointer { } if e.kind == E_STR or e.kind == E_SLICE { return "string" } if e.kind == E_INT { return "int" } + if e.kind == E_FLOAT and is_float_file(e.file) { return "float" } if e.kind == E_FLOAT { return "fixed" } if e.kind == E_BOOL { return "bool" } return null diff --git a/selfhost/frontend/parse.ludic b/selfhost/frontend/parse.ludic index 6d0c013b..8f6b28f1 100644 --- a/selfhost/frontend/parse.ludic +++ b/selfhost/frontend/parse.ludic @@ -11,6 +11,18 @@ var g_game_name: pointer # the `program` name # build tools parse. While parsing, the location is the current token's; while # lowering, it is the statement being emitted (emit_stmt records it). var g_parse_file: pointer = "" # the file whose tokens are being parsed +# `numbers float`: files whose bare decimal literals are float, not fixed. A file that says +# so is listed, and so is every non-runtime file it imports (a barrel passes it on). +var g_float_files: []pointer = new []pointer +function is_float_file(f: pointer) -> bool { + if (f == null) { return false } + var i = 0 + while i < len(g_float_files) { + if (g_float_files[i] == f) { return true } + i += 1 + } + return false +} var g_parsing: bool = true # false once lowering starts var g_err_file: pointer = "" # the statement being lowered var g_err_line: int = 0 @@ -816,6 +828,11 @@ function parse_one_decl() -> void { } skipnl() } + if is_id("numbers") and (toks[pi + 1].text == "float") { + pi += 2 + if not is_float_file(g_parse_file) { push(g_float_files, g_parse_file) } + return + } if is_id("import") { pi += 1 let t = toks[pi] if t.kind != TK_STR { perr("expected \"path\" after import") } @@ -1004,6 +1021,7 @@ function do_import(rel: pointer) -> void { } if already_loaded(full) { return } push(loaded_paths, full) + if is_float_file(g_parse_file) and not is_runtime_path(rel) and not is_float_file(full) { push(g_float_files, full) } if (src == null) { perr(`cannot open import {full}`) } # the audio runtime can arrive through atlas.ludic's own import or the Assets # splice, not only through an Audio.* call in the game; a windowed build must