Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Rust

on:
push:
branches: [ "main", "next" ]
branches: [ "main", "next", "release/**" ]
pull_request:
branches: [ "main", "next" ]
branches: [ "main", "next", "release/**" ]

env:
CARGO_TERM_COLOR: always
Expand Down
5 changes: 1 addition & 4 deletions src/constrain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -891,8 +891,7 @@ pub fn state_machine_json() -> Value {
)
})
.collect();
let states_map: serde_json::Map<String, Value> =
states.into_iter().map(|(k, v)| (k, v)).collect();
let states_map: serde_json::Map<String, Value> = states.into_iter().collect();

json!({
"schemaVersion": 1,
Expand Down Expand Up @@ -1118,7 +1117,6 @@ mod tests {
#[test]
fn logit_masks_toplevel_has_valid_tokens() {
let lm = logit_masks_json();
let vocab = lm["vocabulary"].as_array().unwrap();
let mask = lm["masks"]["TopLevel"].as_array().unwrap();
let count: usize = mask.iter().map(|v| v.as_u64().unwrap() as usize).sum();
assert!(count > 0, "TopLevel must have at least one valid token");
Expand Down Expand Up @@ -1244,7 +1242,6 @@ mod tests {

#[test]
fn completions_after_fn_name() {
let result = completions_at_cursor("fn add ", 1, 8);
// `fn` is a reserved keyword that the parser rejects, but the state
// machine should still track it — `fn` doesn't have a TokenCat, so it's
// skipped and we stay at TopLevel. Use `add` (an ident) instead.
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5529,7 +5529,7 @@ fn check_cmd(
}
} else {
let exp_val = literal_to_value(expected_lit);
if &result != &exp_val {
if result != exp_val {
report_diagnostic(
&enrich(
Diagnostic::error(format!(
Expand Down
50 changes: 49 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5063,11 +5063,59 @@ or write `({fmt_name} \"...\" ...)` so its args are grouped."
ident_span.end > 0 && bang_span.start == ident_span.end
};
if !(is_record || is_field || is_zero_arg_call || is_unwrap) {
// Paren-form call as a call ARGUMENT: `prnt fmt2(3.14, 2)`,
// `prnt at([5 6 7], 1)`. The statement-head and operand
// positions already handle adjacent-paren calls (ILO-544);
// without the same branch here the arity-driven positional
// loop below parses `(3.14, 2)` as a grouped expression and
// dies on the comma with ILO-P003.
let inner_name = name.clone(); // break borrow before pos manipulation
if next == Some(&Token::LParen) {
let paren_name = inner_name.clone();
let saved = self.pos;
self.advance(); // consume the inner function ident
if self.is_adjacent_lparen() && !self.looks_like_inline_lambda() {
let args = self.parse_paren_call_args_for(Some(&paren_name))?;
let call = Expr::Call {
function: paren_name,
args,
unwrap: UnwrapMode::None,
};
// ILO-544: the paren group need not be the whole
// argument list — `at([5 6 7])1` glues the remaining
// operand on. Extend inline up to the known arity
// rather than via `paren_call_atom`, which is only
// consumed by `parse_call_or_atom` and would leak
// into an unrelated atom from here.
let Expr::Call {
function,
mut args,
unwrap,
} = call
else {
unreachable!("just constructed a Call")
};
while args.len() < arity && self.can_start_operand() {
let arg_idx = args.len();
let in_fn_pos = self.is_fn_ref_position(&function, arg_idx);
args.push(self.parse_call_arg(
in_fn_pos,
Some((&function.clone(), arity, arg_idx)),
)?);
}
let call = Expr::Call {
function,
args,
unwrap,
};
return self.parse_field_chain(call, None);
}
self.pos = saved;
}
// ILO-540b: if NO operands follow this known-arity name,
// create a Ref (not a 0-arg Call). This lets local bindings
// that shadow builtins (rev=5; prnt rev) resolve to the
// local variable instead of a mis-dispatched builtin call.
let inner_name = name.clone(); // break borrow before pos manipulation
let next_starts_operand = {
let saved = self.pos;
self.pos = saved + 1;
Expand Down
68 changes: 53 additions & 15 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ struct VerifyContext {
/// binding is at the top (last element). Used by ILO-T048 to detect
/// `x=+x 1`-style rebinds of the loop iterator variable inside `@x` loops.
loop_bindings: Vec<String>,
/// Names bound by top-level `Let` statements in `main`'s body. Used by
/// the ILO-T004 hint (ILO-546): when an undefined variable inside
/// another function is actually a main/script-level binding, the model
/// almost always glued a top-level statement onto that function's line
/// (script mode collects own-line statements into main; same-line
/// statements join the preceding body). Naming the real fix beats the
/// generic enclosing-fn lambda advisory, which sends the model the
/// wrong way for this shape (grade-calculator failed 3/3 on it in the
/// ILO-364 N=5 benchmark).
main_bindings: std::collections::HashSet<String>,
/// Function names whose declaration failed to parse. Populated from
/// `Program.parse_failed_fns` at the start of `verify`. Two effects:
/// 1. We skip type-checking the body of any function in this set (its
Expand Down Expand Up @@ -1496,8 +1506,10 @@ fn builtin_check_args(
}
(Ty::Result(Box::new(Ty::Number), Box::new(Ty::Text)), errors)
}
"abs" | "flr" | "cel" | "rou" | "sqrt" | "log" | "exp" | "sin" | "cos" | "tan"
| "log10" | "log2" | "asin" | "acos" | "atan" => {
"abs" | "flr" | "cel" | "sqrt" | "log" | "exp" | "sin" | "cos" | "tan" | "log10"
| "log2" | "asin" | "acos" | "atan" | "rou"
if !(name == "rou" && arg_types.len() == 2) =>
{
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Number)
{
Expand Down Expand Up @@ -4800,6 +4812,7 @@ impl VerifyContext {
errors: Vec::new(),
in_loop: false,
loop_bindings: Vec::new(),
main_bindings: std::collections::HashSet::new(),
parse_failed_fns: HashMap::new(),
glued_eq_binding_sites: std::collections::HashSet::new(),
suppressed_undef_reported: std::collections::HashSet::new(),
Expand Down Expand Up @@ -4844,6 +4857,19 @@ impl VerifyContext {

/// Phase 1: collect all declarations, check for duplicates and undefined Named types.
fn collect_declarations(&mut self, program: &Program) {
// ILO-546: record main's top-level Let names for the T004 glue hint.
for decl in &program.declarations {
if let Decl::Function { name, body, .. } = decl
&& name == "main"
{
for s in body {
if let crate::ast::Stmt::Let { name: n, .. } = &s.node {
self.main_bindings.insert(n.clone());
}
}
}
}

// Pass 0: collect type aliases (before types so aliases can be used in type fields)
let builtin_type_names = ["n", "t", "b", "L", "R"];
let mut raw_aliases: HashMap<String, Type> = HashMap::new();
Expand Down Expand Up @@ -5616,9 +5642,10 @@ impl VerifyContext {
format!(
"call to '{function}' may violate precondition {precond_str} — add a guard"
),
Some(format!(
Some(
"guard before the call: negate the condition (e.g. for req b!=0, add `=b 0 ^\"...\"` before the call) or wrap in a match on R"
)),
.to_string(),
),
Some(span),
);
}
Expand Down Expand Up @@ -6451,14 +6478,27 @@ impl VerifyContext {
closest_match(name, candidates.iter())
.map(|s| format!("did you mean '{s}'?"))
});
// ILO-504: append hoisting advisory for all undefined
// variables. Nested fn declarations are silently
// hoisted to siblings in single-line form; if the
// model intended a capture, the undefined-variable
// error is the only signal.
base_hint.map(|h| format!(
"{h} (or if '{name}' is from an enclosing fn, use an inline lambda: `(x:n>n;...{name}...)`)"
))
// ILO-546: if the name IS a main/script-level binding,
// the statement referencing it was almost certainly
// glued onto this function's line — script mode only
// collects statements that start their own top-level
// line. Name the one-edit fix instead of the generic
// lambda advisory, which sends the model the wrong way
// for this shape.
if func != "main" && self.main_bindings.contains(name) {
Some(format!(
"'{name}' is bound at top level, but this statement is glued into '{func}''s body — put it on its own line (unindented) so script mode runs it in main"
))
} else {
// ILO-504: append hoisting advisory for all undefined
// variables. Nested fn declarations are silently
// hoisted to siblings in single-line form; if the
// model intended a capture, the undefined-variable
// error is the only signal.
base_hint.map(|h| format!(
"{h} (or if '{name}' is from an enclosing fn, use an inline lambda: `(x:n>n;...{name}...)`)"
))
}
};
self.err(
"ILO-T004",
Expand Down Expand Up @@ -6554,9 +6594,7 @@ impl VerifyContext {
"pst" | "put" | "pat" | "pstx" | "wr" | "padl" | "padr"
) {
"2 or 3".to_string()
} else if callee == "min" || callee == "max" {
"1 or 2".to_string()
} else if callee == "rou" {
} else if matches!(callee.as_str(), "min" | "max" | "rou") {
"1 or 2".to_string()
} else if callee == "run" || callee == "run2" {
"2 or 3".to_string()
Expand Down
11 changes: 8 additions & 3 deletions tests/coverage_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ fn fld_as_decl_name() {

#[test]
fn builtin_as_decl_name() {
fail_code("map=5", "ILO-P011");
// 69565d44 made builtin names legal as BINDINGS: the local shadows the
// builtin in value position, matching Python. Only builtin FN names
// (`builtin_as_fn_name` below) and reserved words still fire ILO-P011.
ok("map=5");
}

#[test]
Expand Down Expand Up @@ -457,8 +460,10 @@ fn stmt_cnt_in_loop() {
}

#[test]
fn stmt_builtin_let_rejected() {
fail_code("f>n;flat=5;flat", "ILO-P011");
fn stmt_builtin_let_accepted() {
// Same shadowing rule inside a fn body (69565d44). Reserved words are
// still rejected there — see `stmt_var_in_body_rejected` below.
ok("f>n;flat=5;flat");
}

#[test]
Expand Down
Loading
Loading