Skip to content

Fix parser, analyzer, and typechecker for documented forms and includes - #550

Merged
logbie merged 5 commits into
mainfrom
claude/wfl-issues-mh4l5m
Jul 2, 2026
Merged

Fix parser, analyzer, and typechecker for documented forms and includes#550
logbie merged 5 commits into
mainfrom
claude/wfl-issues-mh4l5m

Conversation

@logbie

@logbie logbie commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR fixes four reported issues affecting the parser, analyzer, and typechecker:

Key Changes

Parser (src/parser/expr/primary.rs and src/parser/expr/binary.rs)

  • Substring form: Added support for substring of X from START length LEN by accepting optional from and length separators in function call arguments
  • Split form: Added support for split of X by DELIM by optionally consuming of keyword before the text expression
  • Comparison operators: Added support for is not equal to form by consuming optional equal and to keywords after not in binary expressions
  • All changes maintain backward compatibility with existing syntax forms

Analyzer (src/analyzer/mod.rs and src/analyzer/static_analyzer.rs)

TypeChecker (src/typechecker/mod.rs)

Tests (tests/docs_parser_and_include_fixes_test.rs)

  • Added comprehensive regression test suite covering all four issues
  • Tests verify both positive cases (correct parsing/execution) and negative cases (guard against regressions)
  • Includes integration tests that write temporary files and execute the WFL compiler

https://claude.ai/code/session_01VUG4otZGqWhSAvoamQ9G9L

Summary by CodeRabbit

  • New Features
    • Expanded parsing to accept more natural phrasing, including split of X by ..., is not equal to ..., and additional member-call argument separators (including length).
  • Bug Fixes
    • Improved include from behavior so undefined actions can be reported as non-fatal warnings (and type-checking aligns), while truly undefined actions still error when no includes exist.
    • Enhanced function-call validation for builtin shadowing and broadened unused-variable tracking across nested control-flow blocks and more statement types.
  • Tests
    • Added regression tests covering parsing variants, include behavior, warning-vs-error handling, and unused-variable counting.

…, #548, #549)

Addresses five reported issues:

- #549 (parser): accept the documented/natural spellings
  `substring of X from START length LEN`, `split of X by DELIM`, and
  `check if n is not equal to N`. The `of ... and ...` builtin-call
  collector now treats `from`, `by`, and `length` as argument separators;
  the `split` keyword handler optionally consumes a leading `of`; and the
  `is not` comparison branch consumes an optional trailing `equal`/`to`.

- #547 (analyzer): stdlib functions called inside an included file's action
  were reported as "'X' is not a function". The FunctionCall analysis branch
  now recognizes builtin functions before checking scope symbols, matching
  the Variable and ActionCall branches.

- #548 (analyzer/typechecker): calling an include-exposed action from a
  top-level statement raised a fatal "Undefined action" that aborted the
  program before the include ran. Both the analyzer and type checker now
  detect `include from` and suppress undefined-action errors, since included
  files expose actions dynamically at runtime. Undefined actions in programs
  without includes still error as before.

- #468 (analyzer): `add ... to X`, `respond ... with X`, and uses inside
  `main loop`/`try`/`describe`/`test` blocks were not counted as variable
  uses, producing false ANALYZE-UNUSED warnings. Added the missing arms to
  the use-tracking visitor.

- #467 was already fixed on disk (chained `respond ... and status N and
  content_type "X"`); verified end-to-end and covered by existing tests.

Adds tests/docs_parser_and_include_fixes_test.rs covering all cases plus
regression guards. All existing tests pass; fmt and clippy are clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUG4otZGqWhSAvoamQ9G9L
Copilot AI review requested due to automatic review settings July 2, 2026 07:38
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 704728b6-a992-40c9-b2c1-375ecba5c387

📥 Commits

Reviewing files that changed from the base of the PR and between 83d0a37 and 49d5051.

📒 Files selected for processing (3)
  • src/analyzer/mod.rs
  • src/typechecker/mod.rs
  • tests/docs_parser_and_include_fixes_test.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/typechecker/mod.rs
  • src/analyzer/mod.rs

📝 Walkthrough

Walkthrough

This PR adds include-aware diagnostics to semantic analysis and type checking, extends unused-variable tracking into more nested statements, broadens parser acceptance for several phrasings, and adds regression coverage for the updated behavior.

Changes

Language analysis and parser fixes

Layer / File(s) Summary
Analyzer include-aware resolution
src/analyzer/mod.rs
Scans for top-level includes, tracks include presence, changes undefined-action handling, and preserves builtin function-call handling.
TypeChecker include flag and warning emission
src/typechecker/mod.rs, src/analyzer/static_analyzer.rs
Tracks includes in the typechecker, suppresses undefined-action type errors when includes are present, and emits analyzer warnings before handling analysis failures.
Nested unused-variable tracking
src/analyzer/static_analyzer.rs
Expands declaration collection and usage marking into additional nested blocks and more statement kinds that read variables.
Parser phrasing extensions
src/parser/helpers.rs, src/parser/expr/binary.rs, src/parser/expr/primary.rs
Accepts optional of in split forms, parses is not equal to, and broadens separator handling in postfix argument parsing.
Regression tests for parsing and include behavior
tests/docs_parser_and_include_fixes_test.rs
Adds coverage for the new parser spellings, include-exposed actions, undefined-action severity, and unused-variable cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Program
  participant Analyzer
  participant TypeChecker
  Program->>Analyzer: analyze(statements)
  Analyzer->>Analyzer: scan for include from
  Analyzer->>Analyzer: resolve function and action calls
  alt include present
    Analyzer-->>Program: record warning for undefined action
  else no include
    Analyzer-->>Program: report fatal undefined action
  end
  Program->>TypeChecker: check_types(statements)
  TypeChecker->>TypeChecker: scan for include from
  TypeChecker->>TypeChecker: infer action call type
  alt include present
    TypeChecker-->>Program: return Type::Any
  else no include
    TypeChecker-->>Program: report undefined action error
  end
Loading

Related issues: #547, #548, #468
Suggested labels: bug, parser, analyzer, typechecker, tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title is concise and accurately summarizes the main parser, analyzer, and typechecker fixes.
Linked Issues check ✅ Passed The PR addresses the requirements for #547, #548, #468, and #549 with matching parser, analyzer, typechecker, and test changes.
Out of Scope Changes check ✅ Passed No clearly unrelated changes appear; the edits support the documented parser, analyzer, typechecker, and regression-test fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/wfl-issues-mh4l5m

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/parser/expr/primary.rs (1)

1110-1137: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"length" separator check is case-sensitive, unlike similar bareword checks elsewhere in this file.

Line 1121 compares id == "length" directly, whereas the existing "with" bareword check at line 230 uses id.to_lowercase() == "with". A user writing Length instead of length in substring of X from START length LEN will silently fail to match this separator, breaking argument parsing for that call.

🔧 Proposed fix
                                     ) || matches!(
                                         &sep_token.token,
-                                        Token::Identifier(id) if id == "length"
+                                        Token::Identifier(id) if id.to_lowercase() == "length"
                                     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/expr/primary.rs` around lines 1110 - 1137, The separator handling
in parse_primary_expression treats the bareword "length" case-sensitively,
unlike the other bareword checks in this file. Update the Identifier(id) branch
in the separator match to compare case-insensitively the same way the "with"
check does, so variants like "Length" are accepted when parsing stdlib-style
calls such as substring of X from START length LEN.
🧹 Nitpick comments (3)
src/analyzer/static_analyzer.rs (1)

667-731: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Declaration collection doesn't mirror new usage recursion.

mark_used_variables now recurses into MainLoop/ForeverLoop/SingleLineIf/TryStatement/DescribeBlock/TestBlock bodies to mark variables as used, but collect_variable_declarations (lines 372-459) still only recurses into IfStatement, WhileLoop/ForEachLoop/CountLoop, and ActionDefinition. Variables declared inside a main loop, try block, describe/test block, or a single-line if body are never inserted into usages in the first place, so they will silently escape unused-variable detection (a false negative) rather than being properly tracked. This asymmetry means the fix only helps "used elsewhere, declared outside" cases but leaves declarations inside these new block types unchecked.

♻️ Suggested fix: mirror the new recursion in `collect_variable_declarations`
             Statement::WhileLoop { body, .. }
             | Statement::ForEachLoop { body, .. }
             | Statement::CountLoop { body, .. } => {
                 for stmt in body {
                     self.collect_variable_declarations(stmt, usages);
                 }
             }
+            Statement::MainLoop { body, .. } | Statement::ForeverLoop { body, .. } => {
+                for stmt in body {
+                    self.collect_variable_declarations(stmt, usages);
+                }
+            }
+            Statement::SingleLineIf {
+                then_stmt,
+                else_stmt,
+                ..
+            } => {
+                self.collect_variable_declarations(then_stmt, usages);
+                if let Some(else_stmt) = else_stmt {
+                    self.collect_variable_declarations(else_stmt, usages);
+                }
+            }
+            Statement::TryStatement {
+                body,
+                when_clauses,
+                otherwise_block,
+                ..
+            } => {
+                for stmt in body {
+                    self.collect_variable_declarations(stmt, usages);
+                }
+                for clause in when_clauses {
+                    for stmt in &clause.body {
+                        self.collect_variable_declarations(stmt, usages);
+                    }
+                }
+                if let Some(otherwise) = otherwise_block {
+                    for stmt in otherwise {
+                        self.collect_variable_declarations(stmt, usages);
+                    }
+                }
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/analyzer/static_analyzer.rs` around lines 667 - 731,
`collect_variable_declarations` is missing the same nested-block recursion now
added in `mark_used_variables`, so declarations inside `MainLoop`,
`ForeverLoop`, `SingleLineIf`, `TryStatement`, `DescribeBlock`, and `TestBlock`
are never tracked for unused-variable checks. Update
`collect_variable_declarations` in `static_analyzer.rs` to mirror the traversal
logic used by `mark_used_variables`, recursing into those statement variants and
their bodies/clauses so variables declared there are inserted into `usages` and
analyzed consistently.
src/parser/expr/binary.rs (1)

442-449: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated "optional of" consumption logic.

The same optional-of-consumption block is duplicated in src/parser/expr/primary.rs (lines 819-827). Consider extracting a small shared helper (e.g. consume_optional_of(&mut self)) to avoid keeping two copies in sync.

♻️ Example shared helper
+fn consume_optional_of(&mut self) {
+    if let Some(of_token) = self.cursor.peek()
+        && matches!(&of_token.token, Token::KeywordOf)
+    {
+        self.bump_sync();
+    }
+}

Then call self.consume_optional_of(); at both call sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/expr/binary.rs` around lines 442 - 449, The optional "of"
consumption logic is duplicated in both parsing paths, so extract it into a
small shared helper on the parser, such as consume_optional_of, and call that
helper from the existing split-handling code in binary and primary parsing. Keep
the helper responsible only for peeking Token::KeywordOf and bumping it, so both
call sites stay consistent and the logic is maintained in one place.
src/analyzer/mod.rs (1)

372-382: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Include-detection scan is duplicated between Analyzer and TypeChecker.

The same program.statements.iter().any(|s| matches!(s, Statement::IncludeStatement { .. })) scan appears independently in both Analyzer::analyze and TypeChecker::check_types (src/typechecker/mod.rs). Consider extracting a shared helper (e.g., fn program_has_includes(program: &Program) -> bool) to avoid drift if the include-detection logic ever needs to change (e.g., to detect nested includes).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/analyzer/mod.rs` around lines 372 - 382, The include-statement scan is
duplicated in Analyzer::analyze and TypeChecker::check_types, so factor the
shared `program.statements.iter().any(|s| matches!(s,
Statement::IncludeStatement { .. }))` logic into a common helper such as
`program_has_includes(&Program) -> bool`. Update both Analyzer and TypeChecker
to call the helper so include detection stays consistent if the rule changes
later, and keep the existing `has_includes` handling intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/analyzer/mod.rs`:
- Around line 164-168: The undefined-action handling in analyze() is too broad
because has_includes suppresses every Expression::ActionCall “Undefined action”
error whenever any Statement::IncludeStatement exists. Update the logic around
has_includes and the ActionCall diagnostic so include presence only lowers
severity (for example to a warning) or narrows suppression to cases that could
plausibly come from includes, instead of blanket-skipping all undefined actions.
Use the existing analyze(), has_includes, and Expression::ActionCall paths to
keep genuine typos reported while still accounting for dynamically resolved
includes.

---

Outside diff comments:
In `@src/parser/expr/primary.rs`:
- Around line 1110-1137: The separator handling in parse_primary_expression
treats the bareword "length" case-sensitively, unlike the other bareword checks
in this file. Update the Identifier(id) branch in the separator match to compare
case-insensitively the same way the "with" check does, so variants like "Length"
are accepted when parsing stdlib-style calls such as substring of X from START
length LEN.

---

Nitpick comments:
In `@src/analyzer/mod.rs`:
- Around line 372-382: The include-statement scan is duplicated in
Analyzer::analyze and TypeChecker::check_types, so factor the shared
`program.statements.iter().any(|s| matches!(s, Statement::IncludeStatement { ..
}))` logic into a common helper such as `program_has_includes(&Program) ->
bool`. Update both Analyzer and TypeChecker to call the helper so include
detection stays consistent if the rule changes later, and keep the existing
`has_includes` handling intact.

In `@src/analyzer/static_analyzer.rs`:
- Around line 667-731: `collect_variable_declarations` is missing the same
nested-block recursion now added in `mark_used_variables`, so declarations
inside `MainLoop`, `ForeverLoop`, `SingleLineIf`, `TryStatement`,
`DescribeBlock`, and `TestBlock` are never tracked for unused-variable checks.
Update `collect_variable_declarations` in `static_analyzer.rs` to mirror the
traversal logic used by `mark_used_variables`, recursing into those statement
variants and their bodies/clauses so variables declared there are inserted into
`usages` and analyzed consistently.

In `@src/parser/expr/binary.rs`:
- Around line 442-449: The optional "of" consumption logic is duplicated in both
parsing paths, so extract it into a small shared helper on the parser, such as
consume_optional_of, and call that helper from the existing split-handling code
in binary and primary parsing. Keep the helper responsible only for peeking
Token::KeywordOf and bumping it, so both call sites stay consistent and the
logic is maintained in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3837e0e-cc84-4ead-a22b-e982e1280db8

📥 Commits

Reviewing files that changed from the base of the PR and between 7872d0b and 77f6369.

📒 Files selected for processing (6)
  • src/analyzer/mod.rs
  • src/analyzer/static_analyzer.rs
  • src/parser/expr/binary.rs
  • src/parser/expr/primary.rs
  • src/typechecker/mod.rs
  • tests/docs_parser_and_include_fixes_test.rs

Comment thread src/analyzer/mod.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns WFL’s parser/analyzer/typechecker behavior with documented natural-language syntax and improves include-related semantics, while also fixing static analyzer variable-usage tracking to reduce false unused-variable warnings.

Changes:

  • Parser: accepts documented forms for substring ... from ... length ..., split of ... by ..., and is not equal to.
  • Analyzer/TypeChecker: detect include from usage and relax undefined-action reporting/inference to avoid include-driven false failures.
  • Static analyzer + tests: recursively count variable usage in nested blocks and additional statement kinds; adds regression tests covering all four reported issues.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/docs_parser_and_include_fixes_test.rs Adds regression tests for parser forms, include behavior, and unused-variable analysis fixes.
src/typechecker/mod.rs Tracks presence of includes to suppress undefined-action type errors and infer Any for runtime-provided actions.
src/parser/expr/primary.rs Extends call-argument separator parsing and adds optional of handling for split.
src/parser/expr/binary.rs Supports is not equal to token sequence; adds optional of handling for split in binary parsing.
src/analyzer/static_analyzer.rs Recursively marks variables used inside nested statement blocks and additional statement kinds (incl. respond, list ops, loops).
src/analyzer/mod.rs Detects includes; suppresses undefined-action errors when includes are present; adds builtin-guard for include scopes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/analyzer/mod.rs Outdated
Comment on lines +2079 to +2083
if Self::is_builtin_function(name) {
// Built-in stdlib functions (touppercase, wflhash256,
// parse_json, ...) are always callable. When an included
// file is analyzed, parent-scope bindings for these
// natives are injected as plain variables, so without this
Comment thread src/parser/expr/primary.rs
claude and others added 3 commits July 2, 2026 07:59
Follow-ups from CodeRabbit and Copilot review of PR #550:

- analyzer: the builtin-function guard in the FunctionCall branch no longer
  runs before symbol resolution. It now only applies when the resolved symbol
  is a non-function (the include-injected variable case) or is unresolved, so
  arity/signature validation is preserved for builtins registered as real
  Function symbols.
- parser: the `length` argument separator is matched case-insensitively
  (via eq_ignore_ascii_case), consistent with other bareword pseudo-keywords.
- parser: extracted the duplicated optional-`of` consumption into a shared
  Parser::consume_optional_of helper used by both split handlers.
- analyzer/typechecker: extracted the duplicated include-detection scan into a
  shared analyzer::program_has_includes helper.
- static analyzer: collect_variable_declarations now mirrors the recursion in
  mark_used_variables (main loop, forever loop, single-line if, try, describe,
  test blocks) so variables declared inside those blocks are tracked for
  unused-variable analysis.

Adds a regression test for the case-insensitive `length` separator. All tests,
fmt, and clippy pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUG4otZGqWhSAvoamQ9G9L
Previously, when a program used `include from`, all "Undefined action"
errors were fully suppressed to avoid fatally aborting before the include
ran (issue #548). CodeRabbit correctly noted this is too broad: a genuine
typo (e.g. `call grret` for `greet`) in a file that also includes a module
would go completely unreported.

The analyzer now collects such cases as non-fatal warnings instead of
suppressing them. The program still runs to completion (the action may be
provided by an included module at runtime), but genuine typos are surfaced
with a note explaining both possibilities. Programs without includes still
report undefined actions as fatal errors, unchanged. The type checker
continues to infer Type::Any silently so the warning is not duplicated.

Adds a warnings channel + getter to the Analyzer and emits them as
Severity::Warning diagnostics from analyze_static. Updates the include
regression test and adds one asserting typos are surfaced as warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUG4otZGqWhSAvoamQ9G9L
Copilot AI review requested due to automatic review settings July 2, 2026 08:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread src/analyzer/mod.rs Outdated
// callable; only genuinely non-callable symbols are
// an error. Real builtin Function symbols take the
// arm above, so their arity checks are preserved.
if Self::is_builtin_function(name) {
Comment thread src/typechecker/mod.rs Outdated
Comment on lines +195 to +198
// Detect includes: their exposed actions are only known at runtime.
if crate::analyzer::program_has_includes(program) {
self.has_includes = true;
}
Comment thread src/typechecker/mod.rs
Comment on lines +2760 to +2764
} else if self.has_includes {
// Action may be provided by an included file at runtime;
// its result type is unknowable statically, so treat it as
// Any to avoid cascading "could not infer type" errors.
return Type::Any;
Three follow-ups from Copilot's review of PR #550:

- analyzer/typechecker: `has_includes` is now assigned directly from
  program_has_includes() on each run instead of only being set to true, so a
  reused analyzer/typechecker instance (e.g. an editor session) can't carry a
  stale flag from a previous program that used includes.
- analyzer: the builtin-call relaxation is restricted to include-injected
  parent-scope symbols (defined at position 0:0). A user who shadows a builtin
  name with a non-function value (e.g. `store touppercase as "x"`) and then
  calls it still gets an "is not a function" error instead of a silent pass.
- typechecker: in the include-present undefined-action path, each argument
  expression is now inferred before returning Type::Any, so type errors inside
  the arguments are not missed in include-using programs.

Adds a regression test for the builtin-shadowing case. All tests, fmt, and
clippy pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUG4otZGqWhSAvoamQ9G9L
@logbie

logbie commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@logbie
logbie merged commit 1dad16a into main Jul 2, 2026
15 checks passed
@logbie
logbie deleted the claude/wfl-issues-mh4l5m branch July 2, 2026 16:57
logbie added a commit that referenced this pull request Jul 3, 2026
* fix(typechecker): infer builtin return types inside included files (#551)

Follow-on to #550: the analyzer half was fixed, but the type checker
still aborted with a fatal "Could not infer type for variable 'X'" when
an included file bound a builtin-function result to a variable
(store full as wflhash256 of s).

Root cause: the include path injects every parent-environment binding
into the included file's analyzer scope — including stdlib builtins,
whose runtime value is a NativeFunction and therefore gets recorded as
a plain variable symbol with type Unknown at position 0:0. That symbol
shadowed the type checker's builtin-signature fallback, so the call
expression inferred Unknown and the variable declaration errored.

Fixes:
- infer_expression_type's Variable branch now resolves include-injected
  builtin symbols (position 0:0, Unknown/absent type, name registered
  as a builtin) to their real builtin Function signature, mirroring the
  analyzer's #550 treatment. A user who shadows a builtin with a
  concrete value keeps that value's type and the existing behavior.
- get_builtin_function_type gains explicit entries for parse_json
  (Any), stringify_json/stringify_json_pretty (Text), and string_split
  (List<Text>), and its fallback for registered-but-unlisted builtins
  is now Any instead of Unknown, so builtin results stay inferable
  (previously parse_json produced a spurious "Could not infer type"
  even in the main file).

Adds four regression tests to docs_parser_and_include_fixes_test.rs:
wflhash256 and parse_json results stored in variables inside included
actions, a top-level store in an included file, and a main-file
parse_json inference guard.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhKSguwesKWJAGL6ez1Bue

* refactor(typechecker): explicit return types for known builtins

Review follow-up on PR #552: the blanket `_ => Type::Any` fallback in
get_builtin_function_type made builtins with known return types (print,
to_uppercase, generate_uuid, ...) appear to return Any, weakening type
checking — a Nothing-returning call could look usable as a value.

Add explicit entries mirroring the stdlib registrations and runtime
implementations: void functions (print, sleep, foreach, filesystem
mutations) return Nothing; underscore text aliases, generate_uuid,
generate_csrf_token, current_date, format_datetime, and path helpers
return Text; predicates (every, some, pattern_matches, path_matches,
path_exists, ...) return Boolean; sizes/counters return Number;
directory listings and pattern_find_all return lists; path_params
returns a Text->Text map. Genuinely dynamic results (parse_json, query
parsers, DateTime values) are explicitly Any. The Any fallback now only
covers test helpers and not-yet-implemented placeholder names.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhKSguwesKWJAGL6ez1Bue

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants