Skip to content
Closed
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
21 changes: 21 additions & 0 deletions examples/string-aliases.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- string-aliases.ilo: demonstrates muscle-memory long-form aliases for
-- string case-conversion builtins, added in 0.12.1 (ILO-79, ILO-81).
--
-- Canonical names: upr lwr cap
-- Alias names: upper lower capitalize
--
-- On first run with an alias the runtime emits a one-time hint pointing to
-- the canonical short form. Subsequent runs are silent.
--
-- run: demo "> demo"
--
-- Expected output (modulo hint lines on first run):
-- HELLO
-- hello
-- Hello

demo > t
s = "hello"
prnt upper s -- alias for upr: "HELLO"
prnt lower (upr s) -- alias for lwr: back to "hello"
capitalize s -- alias for cap: "Hello"
17 changes: 17 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,8 +504,25 @@ const BUILTIN_ALIASES: &[(&str, &str)] = &[
("flatten", "flat"),
("concat", "cat"),
("contains", "has"),
// `upper`/`lower` mirror the Python/JS/Go/Rust method names for case
// conversion. Canonical 3-char names `upr`/`lwr` stay unchanged in
// bytecode and fmt output; these aliases only rewrite the parse-time
// name so newcomers from those languages don't hit an unknown-builtin
// error on first run.
("upper", "upr"),
("lower", "lwr"),
// `capitalize` mirrors the Python/Ruby method name. Canonical name
// stays `cap`; alias is long-form discoverability only.
("capitalize", "cap"),
("group", "grp"),
("average", "avg"),
// `post` was the canonical HTTP-POST verb name before 0.12.0 when it
// was renamed to the 3-char `pst` to match the short-form convention.
// Personas that learned the language on pre-0.12.0 examples reach for
// `post` as muscle memory; aliasing it back to `pst` closes that gap
// without widening the canonical surface (bytecode, fmt, docs all stay
// on `pst`).
("post", "pst"),
("print", "prnt"),
("trim", "trm"),
("split", "spl"),
Expand Down
122 changes: 122 additions & 0 deletions tests/regression_capitalize_alias.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Regression tests pinning the `capitalize` → `cap` alias contract (ILO-81).
//
// `capitalize` is the standard method name for title-casing the first letter
// in Python and Ruby. Personas from those languages reach for the long form.
// The alias rewrites `capitalize` to canonical `cap` at parse time; bytecode
// and fmt output stay on `cap`.
//
// Contracts to lock in:
// 1. `capitalize` resolves to `cap` at the alias-table level.
// 2. `cap` remains canonical in the builtin registry.
// 3. `capitalize "hello"` runs correctly cross-engine and produces "Hello".
// 4. `capitalize` as a binding name is rejected with ILO-P011.
// 5. A hint mentioning both `capitalize` and `cap` is emitted on first use.

use ilo::ast::resolve_alias;
use ilo::builtins::Builtin;
use std::process::Command;

fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}

fn run(engine: &str, src: &str, entry: &str) -> String {
let out = ilo()
.args([src, engine, entry])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"ilo {engine} {src:?} failed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}

#[cfg(feature = "cranelift")]
const ENGINES_ALL: &[&str] = &["--vm", "--jit"];
#[cfg(not(feature = "cranelift"))]
const ENGINES_ALL: &[&str] = &["--vm"];

#[test]
fn cap_remains_canonical() {
let b = Builtin::from_name("cap").expect("`cap` must be a canonical builtin");
assert_eq!(b.name(), "cap");
assert!(
Builtin::from_name("capitalize").is_none(),
"`capitalize` must not be a canonical name; it is an alias for `cap`"
);
assert_eq!(
resolve_alias("capitalize"),
Some("cap"),
"`capitalize` must resolve to canonical `cap`"
);
}

#[test]
fn capitalize_dispatches_cross_engine() {
for engine in ENGINES_ALL {
let out = run(engine, "f>t;capitalize \"hello\"", "f");
assert_eq!(
out, "Hello",
"{engine}: `capitalize \"hello\"` expected Hello, got {out}"
);
}
}

#[test]
fn cap_canonical_still_works() {
for engine in ENGINES_ALL {
let out = run(engine, "f>t;cap \"world\"", "f");
assert_eq!(out, "World");
}
}

#[test]
fn capitalize_rejected_as_binding_name() {
let out = ilo()
.args(["main>t;capitalize=\"hi\";capitalize"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success());
assert!(
stderr.contains("ILO-P011"),
"expected ILO-P011, got: {stderr}"
);
assert!(
stderr.contains("capitalize") && stderr.contains("cap"),
"error must name alias and canonical, got: {stderr}"
);
}

#[test]
fn capitalize_rejected_as_user_function_name() {
let out = ilo()
.args(["capitalize s:t>t;cap s"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success());
assert!(
stderr.contains("ILO-P011"),
"expected ILO-P011, got: {stderr}"
);
}

#[test]
fn capitalize_emits_canonical_hint() {
let out = ilo()
.args(["f>t;capitalize \"world\"", "--vm", "f"])
.output()
.expect("failed to run ilo");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
combined.contains("capitalize") && combined.contains("cap"),
"expected hint mentioning `capitalize` and `cap`, got: {combined}"
);
}
84 changes: 84 additions & 0 deletions tests/regression_post_alias.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Regression tests pinning the `post` → `pst` alias contract (ILO-78).
//
// `post` was the canonical HTTP-POST verb name before 0.12.0 when it was
// renamed to the 3-char `pst` to match the short-form convention. Users who
// learned the language pre-0.12.0 have `post` as muscle memory. The alias
// resolves `post` → `pst` at parse time so those users get a canonical-name
// hint on first run and keep working without modification.
//
// Contracts to lock in:
// 1. `post` resolves to `pst` at the alias-table level.
// 2. `pst` remains the canonical name in the builtin registry.
// 3. `post` as a binding name is rejected at parse time with ILO-P011.
// 4. A hint mentioning both `post` and `pst` is emitted on first use.

use ilo::ast::resolve_alias;
use ilo::builtins::Builtin;
use std::process::Command;

fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}

#[test]
fn pst_remains_the_canonical_name() {
let b = Builtin::from_name("pst").expect("`pst` must be a canonical builtin");
assert_eq!(b.name(), "pst", "canonical name is `pst`");
assert!(
Builtin::from_name("post").is_none(),
"`post` must not be a canonical name; it is an alias for `pst`"
);
assert_eq!(
resolve_alias("post"),
Some("pst"),
"`post` must resolve to canonical `pst`"
);
}

#[test]
fn post_rejected_as_binding_name() {
let out = ilo()
.args(["main>t;post=\"body\";post"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"expected `post=\"body\"` to fail at parse time"
);
assert!(
stderr.contains("ILO-P011"),
"expected ILO-P011 reserved-name error, got: {stderr}"
);
assert!(
stderr.contains("post") && stderr.contains("pst"),
"error must name the alias and the canonical builtin, got: {stderr}"
);
}

#[test]
fn post_rejected_as_user_function_name() {
let out = ilo()
.args(["post url:t body:t>R t t;pst url body"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"expected `post url:t body:t>...` to fail at parse time"
);
assert!(
stderr.contains("ILO-P011"),
"expected ILO-P011 reserved-name error, got: {stderr}"
);
}

#[test]
fn post_alias_hint_data_is_correct() {
// The hint system calls `resolve_alias(word)` on each lexed identifier and
// emits "hint: `word` → `canonical` (canonical form)" on successful runs.
// We verify the alias-table data that drives the hint rather than firing a
// real HTTP request in tests (which would be network-dependent).
let alias = resolve_alias("post").expect("`post` must be in BUILTIN_ALIASES");
assert_eq!(alias, "pst", "hint would say `post` → `pst`");
}
Loading
Loading