Skip to content
Merged
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
12 changes: 12 additions & 0 deletions examples/fld-reserved-rename.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- fld is reserved as the fold builtin. Using it as a variable name
-- triggers ILO-P011 with a rename suggestion, not a misleading
-- arity-mismatch cascade from the builtin call site.
--
-- Correct shape: pick a different name like field or folder.

countup n:n>n;field=0;@i 0..n{field=+field 1};field

-- run: countup 5
-- out: 5
-- run: countup 10
-- out: 10
23 changes: 23 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,19 @@ impl Parser {
format!("pick a different name like `{alt}` or `{}`", &word[..1]),
));
}
// Builtin `fld` (fold) used as binding name: `fld=5`. Personas reach
// for `fld` as a natural variable (field/fold/folder); the builtin
// collision otherwise surfaces as a misleading ILO-T006 arity error.
if let Some(Token::Ident(name)) = self.peek()
&& name == "fld"
&& self.token_at(self.pos + 1) == Some(&Token::Eq)
{
return Err(self.error_hint(
"ILO-P011",
"`fld` is reserved for the fold builtin and cannot be used as an identifier".into(),
"pick a different name like `field` or `folder`".into(),
));
}
match self.peek() {
Some(Token::Type) => self.parse_type_decl(),
Some(Token::Tool) => self.parse_tool_decl(),
Expand Down Expand Up @@ -801,6 +814,16 @@ impl Parser {
self.advance(); // consume "cnt"
Ok(Stmt::Continue)
}
Some(Token::Ident(name))
if name == "fld" && self.token_at(self.pos + 1) == Some(&Token::Eq) =>
{
Err(self.error_hint(
"ILO-P011",
"`fld` is reserved for the fold builtin and cannot be used as an identifier"
.into(),
"pick a different name like `field` or `folder`".into(),
))
}
Some(Token::Ident(name)) if name == "wh" => {
self.advance(); // consume "wh"
let condition = self.parse_expr()?;
Expand Down
128 changes: 128 additions & 0 deletions tests/regression_fld_reserved.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Regression: `fld` used as a binding name must surface the friendly
// ILO-P011 reserved-word error, not a misleading ILO-T006 arity mismatch
// from the fold builtin. Mirrors the `cnt`/`brk` handling from commit
// 8928635. Personas reach for `fld` as a natural variable name (field /
// fold / folder) and previously paid the retry tax on a cryptic error.
//
// The fix lives in the parser, so all engines surface the same error.
// Tests confirm each engine's CLI path emits ILO-P011 with the right
// wording.

use std::process::Command;

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

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

// `fld=5` at top level inside a function body.
const FLD_BINDING_IN_BODY: &str = "f>n;fld=5;fld";

fn check_fld_binding(engine: &str) {
let (ok, stderr) = run(engine, FLD_BINDING_IN_BODY, "f");
assert!(!ok, "engine={engine}: expected parse failure");
assert!(
stderr.contains("ILO-P011"),
"engine={engine}: missing ILO-P011, stderr={stderr}"
);
assert!(
stderr.contains("`fld` is reserved for the fold builtin"),
"engine={engine}: missing friendly message, stderr={stderr}"
);
assert!(
stderr.contains("field") || stderr.contains("folder"),
"engine={engine}: hint should suggest field/folder, stderr={stderr}"
);
// Should not cascade into the verifier's misleading arity error.
assert!(
!stderr.contains("ILO-T006"),
"engine={engine}: arity cascade leaked, stderr={stderr}"
);
assert!(
!stderr.contains("arity mismatch"),
"engine={engine}: arity cascade leaked, stderr={stderr}"
);
}

#[test]
fn fld_binding_in_body_tree() {
check_fld_binding("--run-tree");
}

#[test]
fn fld_binding_in_body_vm() {
check_fld_binding("--run-vm");
}

#[test]
#[cfg(feature = "cranelift")]
fn fld_binding_in_body_cranelift() {
check_fld_binding("--run-cranelift");
}

// `fld=5` inside a loop body, the natural shape a persona writes when
// accumulating a fold-style value across iterations.
const FLD_BINDING_IN_LOOP: &str = "f a:n>n;@i 0..a{fld=i};1";

fn check_fld_binding_loop(engine: &str) {
let (ok, stderr) = run(engine, FLD_BINDING_IN_LOOP, "f");
assert!(!ok, "engine={engine}: expected parse failure");
assert!(
stderr.contains("ILO-P011"),
"engine={engine}: missing ILO-P011, stderr={stderr}"
);
assert!(
stderr.contains("`fld` is reserved for the fold builtin"),
"engine={engine}: missing friendly message, stderr={stderr}"
);
assert!(
!stderr.contains("ILO-T006"),
"engine={engine}: arity cascade leaked, stderr={stderr}"
);
}

#[test]
fn fld_binding_in_loop_tree() {
check_fld_binding_loop("--run-tree");
}

#[test]
fn fld_binding_in_loop_vm() {
check_fld_binding_loop("--run-vm");
}

#[test]
#[cfg(feature = "cranelift")]
fn fld_binding_in_loop_cranelift() {
check_fld_binding_loop("--run-cranelift");
}

// Sanity: `fld` as the fold builtin still works after the fix.
#[test]
fn fld_as_builtin_still_works() {
let out = ilo()
.args([
"add x:n y:n>n;+x y;f>n;fld add [1 2 3 4] 0",
"--run-tree",
"f",
])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"fld builtin broken: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("10"), "expected 10, got: {stdout}");
}
17 changes: 17 additions & 0 deletions tests/regression_friendly_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ fn friendly_brk_binding() {
assert!(!err.contains("ILO-T028"), "cascade leaked: {err}");
}

#[test]
fn friendly_fld_binding() {
let err = run_err("fld=5");
assert!(err.contains("ILO-P011"), "stderr: {err}");
assert!(
err.contains("`fld` is reserved for the fold builtin"),
"stderr: {err}"
);
assert!(err.contains("field"), "hint should suggest `field`: {err}");
// Should not cascade through to the verifier's misleading arity error.
assert!(!err.contains("ILO-T006"), "arity cascade leaked: {err}");
assert!(
!err.contains("arity mismatch"),
"arity cascade leaked: {err}"
);
}

// ---- Underscore mid-identifier ----

#[test]
Expand Down
Loading