Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
422 changes: 422 additions & 0 deletions 03_PROTO/crates/riina-codegen/src/emit.rs

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions 03_PROTO/crates/riina-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,29 @@ pub fn wasm_supports_builtin(name: &str) -> bool {
pub fn codegen_supports_builtin(name: &str) -> bool {
lower::builtin_canonical(name).is_some()
}

/// Whether the reference INTERPRETER binds `name` — i.e. whether `riinac run`
/// can execute a call to it.
///
/// This exists because "the typechecker accepts it" and "something can run it"
/// are different questions, and the Backend column in `docs/api/STDLIB.md` was
/// silently conflating them. Its `interp-only` cell promised "`riinac run`
/// only", which for the eight crypto-agility builtins
/// (`guna_kripto`/`use_crypto`, `pilih_algo`/`select_algorithm`,
/// `cipher`/`sifer`, `hash_dengan`/`hash_with`) was FALSE: they are registered
/// in the typechecker with `Fn(Teks, Any, Kripto)` and carry the REQ-48
/// deprecation check at their call sites, but no runtime binds them, so
/// `riinac run` fails with `unbound variable` exactly as `riinac build` does.
/// A doc that says a builtin runs somewhere it does not is worse than one that
/// says nothing, so the column now distinguishes the two.
///
/// Derived by building the real interpreter environment and looking the name
/// up, not from a list — a list would drift the moment a runtime is added.
pub fn interpreter_supports_builtin(name: &str) -> bool {
builtins::register_builtins(&value::Env::new())
.lookup(name)
.is_some()
}
pub use value::Value;

/// Result type for code generation operations
Expand Down
43 changes: 42 additions & 1 deletion 03_PROTO/crates/riina-codegen/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ pub(crate) fn builtin_canonical(name: &str) -> Option<&'static str> {
// I/O
match name {
"cetak" | "print" => return Some("cetak"),
"cetakln" | "println" => return Some("cetakln"),
// `cetak_baris` is a third spelling of `cetakln` — the interpreter binds
// all three to the same `Value::Builtin("cetakln")`. Only this gate did
// not know it, so a program using that spelling ran and then failed to
// build. A pure aliasing gap, not a missing implementation.
"cetakln" | "println" | "cetak_baris" => return Some("cetakln"),
// String
"gabung_teks" | "concat" => return Some("gabung_teks"),
"panjang" | "length" => return Some("panjang"),
Expand All @@ -105,6 +109,43 @@ pub(crate) fn builtin_canonical(name: &str) -> Option<&'static str> {
"punca" | "sqrt" => return Some("punca"),
"gcd" => return Some("gcd"),
"lcm" => return Some("lcm"),
// `baki`/`rem` and `log2` had C implementations in emit.rs all along;
// only this gate was missing, so they were unreachable from a compiled
// program. `baki` additionally needed its TYPE corrected — it was
// declared unary and so could not be called from any well-typed program
// at all (see the note in riina-typechecker's math section).
"baki" | "rem" => return Some("baki"),
"log2" => return Some("log2"),
// `rawak`/`random`. Routed even though the two backends CANNOT be held
// to byte equality — both are time-seeded, so a differential can only
// pin the range invariant, and `pure_builtin_differential` says so
// rather than pretending otherwise. Routing is still right: without it
// a compiled RIINA program has no source of randomness at all, and
// neither implementation claims to be a CSPRNG (the interpreter hashes
// the clock, the emitted C runs an LCG). Anything needing cryptographic
// randomness must come from `riina-core`, not from here.
"rawak" | "random" => return Some("rawak"),
// Range constructors behind the `a..b` / `a..=b` surface syntax.
"julat" => return Some("julat"),
"julat_inklusif" => return Some("julat_inklusif"),
// Sum introspection. COMPILER INTERNALS, not stdlib: the parser's
// if-chain pattern compiler emits these for a constructor pattern nested
// where a `Case` cannot go, such as `(Ada(a), Tiada)` inside a tuple
// pattern. Leaving them unrouted meant that whole class of pattern ran
// under `riinac run` and failed `riinac build` with
// `unbound variable: nilai_kiri` — a language feature that did not
// compile, wearing the costume of a missing builtin.
// Unicode. These three are why `emit.rs` gained its ONE conditional
// prelude block: they need ~250 KB of vendored UCD tables, which would
// otherwise more than double every compiled binary to serve builtins
// most programs never call. See `emit_unicode_runtime`.
"nfc" | "ke_nfc" => return Some("nfc"),
"skeleton" | "rangka" => return Some("skeleton"),
"adalah_keliru" | "is_confusable" => return Some("adalah_keliru"),
"adalah_kiri" => return Some("adalah_kiri"),
"adalah_kanan" => return Some("adalah_kanan"),
"nilai_kiri" => return Some("nilai_kiri"),
"nilai_kanan" => return Some("nilai_kanan"),
// Test
"tegaskan" | "assert" => return Some("tegaskan"),
"tegaskan_sama" | "assert_eq" => return Some("tegaskan_sama"),
Expand Down
29 changes: 22 additions & 7 deletions 03_PROTO/crates/riina-typechecker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1486,16 +1486,31 @@ pub fn register_builtin_types(ctx: &Context) -> Context {
}

// ── Extra math builtins ──
for (bm, en) in &[("baki", "rem"), ("log2", "log2")] {
//
// `baki`/`rem` are BINARY and take a pair, like `minimum`/`maksimum`/`kuasa`.
// They were previously typed `Nombor -> Nombor` because they shared this
// loop with the genuinely unary `log2`, which made them **uncallable from
// any well-typed program**: `baki(10, 3)` is `App(App(baki, 10), 3)` and
// failed with "Expected function type, found Int", while `baki((10, 3))`
// failed with "expected Int, found Prod(Int, Int)". Both the interpreter
// (`matematik.rs`, `extract_pair_ints`) and the emitted C
// (`riina_builtin_baki`, which requires `RIINA_TAG_PAIR`) have always taken
// a pair, so the type was the single odd one out.
for nm in ["baki", "rem"] {
c = c.extend(
bm.to_string(),
Ty::Fn(Box::new(Ty::Int), Box::new(Ty::Int), Effect::Pure),
);
c = c.extend(
en.to_string(),
Ty::Fn(Box::new(Ty::Int), Box::new(Ty::Int), Effect::Pure),
nm.to_string(),
Ty::Fn(
Box::new(Ty::Prod(Box::new(Ty::Int), Box::new(Ty::Int))),
Box::new(Ty::Int),
Effect::Pure,
),
);
}
// `log2` really is unary — it is what the loop above was written for.
c = c.extend(
"log2".to_string(),
Ty::Fn(Box::new(Ty::Int), Box::new(Ty::Int), Effect::Pure),
);
// Random — Effect::Random
c = c.extend(
"rawak".to_string(),
Expand Down
38 changes: 30 additions & 8 deletions 03_PROTO/crates/riina-typechecker/tests/stdlib_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
//! Regenerate after changing the builtin table:
//! REGEN_STDLIB_DOC=1 cargo test -p riina-typechecker --test stdlib_doc

use riina_codegen::{codegen_supports_builtin, wasm_supports_builtin};
use riina_codegen::{
codegen_supports_builtin, interpreter_supports_builtin, wasm_supports_builtin,
};
use riina_fmt::format_ty;
use riina_typechecker::{register_builtin_types, Context};
use riina_types::{Effect, Ty};
Expand All @@ -29,8 +31,19 @@ enum Backend {
Wasm,
/// Compiles to C; the WASM backend refuses it (fails closed).
Native,
/// Neither: `riinac run` only.
/// Neither compiles, but the interpreter binds it: `riinac run` only.
Interp,
/// NOTHING runs it. The typechecker accepts the call — the name is in the
/// builtin type registry — but no interpreter or backend binds it, so every
/// path fails with `unbound variable`.
///
/// This state was added because `interp-only` was making a FALSE promise for
/// the eight crypto-agility builtins: its cell reads "`riinac run` only",
/// and `riinac run` cannot run them either. A three-state column had no way
/// to say "typed but unimplemented", so it said the nearest thing, which was
/// wrong. A doc claiming a builtin runs somewhere it does not is worse than
/// one that says nothing.
TypedOnly,
}

impl Backend {
Expand All @@ -39,8 +52,10 @@ impl Backend {
Self::Wasm
} else if codegen_supports_builtin(name) {
Self::Native
} else {
} else if interpreter_supports_builtin(name) {
Self::Interp
} else {
Self::TypedOnly
}
}

Expand All @@ -49,6 +64,7 @@ impl Backend {
Self::Wasm => "compiled",
Self::Native => "**native-only**",
Self::Interp => "**interp-only**",
Self::TypedOnly => "**typed-only**",
}
}
}
Expand Down Expand Up @@ -112,7 +128,11 @@ fn generate() -> String {
let wasm_count = rows.iter().filter(|(_, _, _, b)| *b == Backend::Wasm).count();
let native_count = rows.iter().filter(|(_, _, _, b)| *b == Backend::Native).count();
let compiled_count = wasm_count + native_count;
let interp_only_count = rows.len() - compiled_count;
let interp_only_count = rows.iter().filter(|(_, _, _, b)| *b == Backend::Interp).count();
let typed_only_count = rows
.iter()
.filter(|(_, _, _, b)| *b == Backend::TypedOnly)
.count();

// Group by effect; render an effect section ordered by the effect enum.
let effect_order = [
Expand Down Expand Up @@ -163,13 +183,14 @@ fn generate() -> String {
// then cannot be built. State it before the tables, not after.
out.push_str(&format!(
"## ⚠ Read first: type-checking does not imply compiling\n\n\
Every builtin below type-checks and runs under `riinac run` (the \
interpreter). Only **{compiled}** of the {total} also compile, and they \
do NOT all reach the same backends:\n\n\
Every builtin below type-checks. That is ALL it means: type-checking \
does not imply compiling, and it does not even imply running. \
**{compiled}** of the {total} compile:\n\n\
| Backend value | Meaning |\n|---|---|\n\
| `compiled` | Lowers to C **and** WASM ({wasm} builtins). |\n\
| `native-only` | Lowers to C. The WASM backend **refuses** it ({native} builtins). |\n\
| `interp-only` | `riinac run` only ({interp} builtins). `riinac build` fails with `unbound variable`. |\n\n\
| `interp-only` | `riinac run` only ({interp} builtins). `riinac build` fails with `unbound variable`. |\n\
| `typed-only` | **Nothing runs it** ({typed} builtins). The name is in the type registry, but neither the interpreter nor any backend binds it, so `riinac run` fails with `unbound variable` too. |\n\n\
```\n\
$ riinac check baca.rii # Success! Effect: FileSystem\n\
$ riinac run baca.rii # works — reads the file\n\
Expand Down Expand Up @@ -198,6 +219,7 @@ fn generate() -> String {
native = native_count,
total = rows.len(),
interp = interp_only_count,
typed = typed_only_count,
));
out.push_str(
"*Scope note:* this lists the language builtins the typechecker installs. \
Expand Down
Loading
Loading