Summary
An include from module's variable can be read (warning only, works) and can be overwritten with store (no diagnostic, works, and really does mutate the shared binding) — but change-ing it is a fatal analyzer error that stops the program before it runs. The include-aware relaxation added by #548/#580/#592 covers callee/name references only and was never extended to assignment targets, so the one spelling WFL tells users to use everywhere else is the one spelling that fails.
Reproduction
inc_lib.wfl:
l3_include_readonly.wfl:
include from "inc_lib.wfl"
display shared_flag
change shared_flag to yes
display shared_flag
Command:
wfl l3_include_readonly.wfl
Expected
no then yes, exit 0.
Docs/04-advanced-features/modules.md:98 lists, under "Use include from for":
"Configuration - Constants and shared variables", and the Comparison Example
at :125-127 shows an included store utility_value as "shared utility" being
read from the includer under the heading "Variables are also available". The
docs never state that included variables are read-only, and include from is
explicitly the mechanism that "actually shares" definitions (contrasted with
load module at :23-26).
Actual
warning[ANALYZE-SEMANTIC]: Undefined action 'shared_flag'
= This action is not defined in this file; it may be provided by an included module at runtime, otherwise this is likely a typo.
warning[ANALYZE-SEMANTIC]: Undefined action 'shared_flag'
= This action is not defined in this file; it may be provided by an included module at runtime, otherwise this is likely a typo.
error[ANALYZE-SEMANTIC]: Variable 'shared_flag' is not defined
Exit code: 3. Nothing runs — not even the first display.
Removing only the change line leaves a warning and a working program that
prints no, exit 0. So reading is permitted and writing is fatal.
The asymmetry is an oversight, not a design decision — three pieces of evidence
1. The runtime already supports exactly what the analyzer forbids. An action
defined in the included file can mutate the module variable, and the caller sees
it:
// inc_lib2.wfl
store counter_val as 0
define action called bump_counter:
change counter_val to counter_val plus 1
end action
include from "inc_lib2.wfl"
call bump_counter
call bump_counter
display counter_val // prints 2, exit 0
The shared binding is genuinely mutable at runtime. Only the static check blocks
the direct spelling.
2. store on the same name is accepted and does the same thing. It is not a
shadow — the module's own action observes the new value:
include from "inc_lib3.wfl" // store shared_val as "original"
display "caller sees: " with shared_val // original
store shared_val as "overwritten"
display "caller sees: " with shared_val // overwritten
call show_shared // module sees: overwritten
Exit 0, no error. So the fatal path can be bypassed by using the spelling WFL
normally rejects — in any other context the analyzer insists on the opposite:
error[ANALYZE-SEMANTIC]: Variable 'counter' has already been defined in an outer
scope. Use 'change counter to <value>' to modify it.
Following that guidance across an include from boundary produces a hard error.
The advice is exactly inverted, and a user who learns the rule correctly is the
one who gets stuck.
3. The relaxation in the source is explicitly scoped to callees. The only
include-aware downgrade is:
// src/analyzer/mod.rs:897-913
fn warn_undefined_callee_if_includes(&mut self, name: &str, line: usize, column: usize) -> bool {
if self.has_includes {
self.warnings.push(SemanticError::new(format!("Undefined action '{name}'"), line, column));
true
} else { false }
}
gated on has_includes, whose own doc comment says
"undefined-action errors are downgraded to warnings" (src/analyzer/mod.rs:392-395).
Assignment-target resolution never calls it. Nothing anywhere expresses an intent
that included variables be immutable — there is no "included variables are
read-only" diagnostic, just the generic "is not defined".
Secondary: the read-path message is wrong
Reading a variable reports Undefined action 'shared_flag' with a note about
actions. For a variable reference that is misleading on its face, and it is the
message a user sees on the path that actually works.
Deduplication / lineage
This is the next uncovered case in an existing sequence, not a duplicate:
Searched include from variable change, include shared variable,
linter false positive, and the new #698-#705 — no existing issue covers the
write path.
Interaction with #701
#701 (container actions cannot call sibling actions) and this issue together mean
there is no supported way to share mutable state across files: containers
cannot host cooperating methods, and an included module's variable cannot be
written from the includer. The documented workaround for this issue — put a setter
action in the module and call it — works (evidence 1 above) and is what a port
would use, but it is a workaround for a check that should not fire.
Environment
- wfl --version:
WebFirst Language (WFL) version 26.8.4
- binary: system install
C:\Program Files\wfl\bin\wfl.exe
- commit: c277d8f
- OS: Windows 11 Pro 10.0.26200
- build: release
Also reproduces identically on the repo build G:\repos\wfl\target\release\wfl.exe
(version 26.8.2, same fatal error, exit 3) — not a regression.
No .wflcfg in scope for the repro.
Context
Found while porting G:/repos/JShrink/src/JShrink/Minifier.php (a 738-line PHP
JavaScript minifier) to WFL. The port holds the minifier's ten mutable state
fields as module-level variables — the architecture forced by #701 — so splitting
that state into an included module is the natural way to break up a 780-line file.
This did not block the port: it was kept in one file, so nothing was stubbed.
It is filed because the failure mode is bad for a beginner — the error text
("Variable 'shared_flag' is not defined") points at a name that demonstrably is
defined and readable two lines earlier, gives no hint that the include boundary is
involved, and is contradicted by the same analyzer's usual advice to use change.
Summary
An
include frommodule's variable can be read (warning only, works) and can be overwritten withstore(no diagnostic, works, and really does mutate the shared binding) — butchange-ing it is a fatal analyzer error that stops the program before it runs. The include-aware relaxation added by #548/#580/#592 covers callee/name references only and was never extended to assignment targets, so the one spelling WFL tells users to use everywhere else is the one spelling that fails.Reproduction
inc_lib.wfl:l3_include_readonly.wfl:Command:
Expected
nothenyes, exit 0.Docs/04-advanced-features/modules.md:98lists, under "Useinclude fromfor":"Configuration - Constants and shared variables", and the Comparison Example
at
:125-127shows an includedstore utility_value as "shared utility"beingread from the includer under the heading "Variables are also available". The
docs never state that included variables are read-only, and
include fromisexplicitly the mechanism that "actually shares" definitions (contrasted with
load moduleat:23-26).Actual
Exit code: 3. Nothing runs — not even the first
display.Removing only the
changeline leaves a warning and a working program thatprints
no, exit 0. So reading is permitted and writing is fatal.The asymmetry is an oversight, not a design decision — three pieces of evidence
1. The runtime already supports exactly what the analyzer forbids. An action
defined in the included file can mutate the module variable, and the caller sees
it:
The shared binding is genuinely mutable at runtime. Only the static check blocks
the direct spelling.
2.
storeon the same name is accepted and does the same thing. It is not ashadow — the module's own action observes the new value:
Exit 0, no error. So the fatal path can be bypassed by using the spelling WFL
normally rejects — in any other context the analyzer insists on the opposite:
Following that guidance across an
include fromboundary produces a hard error.The advice is exactly inverted, and a user who learns the rule correctly is the
one who gets stuck.
3. The relaxation in the source is explicitly scoped to callees. The only
include-aware downgrade is:
gated on
has_includes, whose own doc comment says"undefined-action errors are downgraded to warnings" (
src/analyzer/mod.rs:392-395).Assignment-target resolution never calls it. Nothing anywhere expresses an intent
that included variables be immutable — there is no "included variables are
read-only" diagnostic, just the generic "is not defined".
Secondary: the read-path message is wrong
Reading a variable reports
Undefined action 'shared_flag'with a note aboutactions. For a variable reference that is misleading on its face, and it is the
message a user sees on the path that actually works.
Deduplication / lineage
This is the next uncovered case in an existing sequence, not a duplicate:
ActionCallfor include-exposed names (closed)<action> of <args>to an include-exposed action is still context-dependently fatal (works inmain loop/tests, fatal at top level & in action bodies) #580 extended it to the<action> of <args>form (closed)Variable '<name>' is not defined) — the call form #580's fix didn't cover #592 extended it to bare zero-arg name references (closed) — that fix is whythe
display shared_flagline above is only a warning todaychange <name> to …) were never coveredSearched
include from variable change,include shared variable,linter false positive, and the new #698-#705 — no existing issue covers thewrite path.
Interaction with #701
#701 (container actions cannot call sibling actions) and this issue together mean
there is no supported way to share mutable state across files: containers
cannot host cooperating methods, and an included module's variable cannot be
written from the includer. The documented workaround for this issue — put a setter
action in the module and call it — works (evidence 1 above) and is what a port
would use, but it is a workaround for a check that should not fire.
Environment
WebFirst Language (WFL) version 26.8.4C:\Program Files\wfl\bin\wfl.exeAlso reproduces identically on the repo build
G:\repos\wfl\target\release\wfl.exe(version 26.8.2, same fatal error, exit 3) — not a regression.
No
.wflcfgin scope for the repro.Context
Found while porting
G:/repos/JShrink/src/JShrink/Minifier.php(a 738-line PHPJavaScript minifier) to WFL. The port holds the minifier's ten mutable state
fields as module-level variables — the architecture forced by #701 — so splitting
that state into an included module is the natural way to break up a 780-line file.
This did not block the port: it was kept in one file, so nothing was stubbed.
It is filed because the failure mode is bad for a beginner — the error text
("Variable 'shared_flag' is not defined") points at a name that demonstrably is
defined and readable two lines earlier, gives no hint that the include boundary is
involved, and is contradicted by the same analyzer's usual advice to use
change.