fix: restore the rules the M7 half never had to implement

Unifying the two engines exposed what the split had been hiding: every rule
that lived only in the M6 body was silently dropped for units the M7 half
claimed, and since M1-M6 sources never reached that half, nothing failed until
they all did. Eleven cases across m3, m5 and m6 caught it.

Checker, all from the M6 statement and lvalue cases:

- writing a struct field needs a writable place, so `p.x = 3` on a `let` is an
  error again (m3-badfield)
- `let` cannot bind a mutable slice, a var with no initializer needs a type,
  and a void expression cannot initialize (m3-bad-mlet)
- a returned reference must derive from a parameter or a static, and a void
  expression cannot be returned from a value function (m6-badarg, badret,
  badself, badtwo, badlocsl)
- rebinding a reference must not outlive its source scope, and must release the
  previous borrow (m6-badscop)
- the loop case delegates to the core, which carries the flow capture and merge
  that detects a value moved on every iteration; the M7 version had none of it
  (m5-bad-loop)

Emitter:

- builtins other than the print family (@size_of, @align_of) and the str alias
  methods are lowered by the core, which the M7 call path never reached, so
  they were emitted verbatim into the C (m3-struct)
- the trim helper is emitted from the M7 type-helper pass as well, not only the
  core one, or the call has no definition to link (m6-oktrim)

M1-M7 all green: 16, 19, 50, 20, 16, 57, 42.
This commit is contained in:
2026-08-17 02:07:26 +09:00
parent eb85e9fa3d
commit ee2b417013
2 changed files with 70 additions and 11 deletions
+48 -11
View File
@@ -2247,6 +2247,12 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read)
if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) {
if (base->kind==FE_TYPE_REF && !base->ref_mut)
err(s->c,n->loc,"cannot write through shared reference");
/* Writing a field still needs a writable place. This branch used to
be reached only by units mentioning M7 syntax, so it never had to
repeat the check the M6 path does. */
if (base->kind!=FE_TYPE_REF && base->kind!=FE_TYPE_OWNED &&
!lvalue_writable(s,n->a))
err(s->c,n->loc,"cannot assign through immutable value");
field=fe_type_field(owner,n->b->text);
if (!field) {
err(s->c,n->loc,"assignment requires a valid struct field");
@@ -2485,6 +2491,15 @@ static void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable)
if (n->b && !fe_type_equal(expected,stored) &&
!m7_actual_compatible(expected,stored,n->b))
err(s->c,n->loc,"initializer type mismatch");
/* Rules the M6 declaration case carried that this one has to repeat now
that it is the only declaration case. */
if (n->b && stored && stored->kind==FE_TYPE_VOID)
err(s->c,n->loc,"void expression cannot initialize a variable");
if (!mutable && expected && expected->kind==FE_TYPE_SLICE &&
expected->ref_mut)
err(s->c,n->loc,"let cannot bind a mutable slice");
if (!n->b && !n->a)
err(s->c,n->loc,"uninitialized var requires an explicit type");
if (n->b) mark_moved(s,n->b,actual);
initialized=n->b!=0;
sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized,
@@ -2542,6 +2557,25 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
sym->initialized=1;
fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc);
sym->moved=sym->own.move;
/* Rebinding a reference, from the M6 assignment case: the new
source has to live at least as long as the reference does, and
the previous borrow has to be released. */
if (n->b && n->b->kind==FE_N_UNARY && n->b->text &&
(strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) &&
fe_own_is_reference_like(sym->type)) {
FeSym *root=own_root_symbol(s,n->b->a);
if (root && root->owner!=sym->owner)
err(s->c,n->b->loc,"reference would outlive its source scope");
else if (root) {
if (sym->borrow_root) {
if (sym->borrow_mut)
fe_own_release_exclusive(&sym->borrow_root->own);
else fe_own_release_shared(&sym->borrow_root->own);
}
sym->borrow_root=root;
sym->borrow_mut=strcmp(n->b->text,"&mut")==0;
}
}
}
break;
case FE_N_EXPR_STMT:
@@ -2614,6 +2648,15 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
else
stored=fe_type_intern(&s->c->types,"void");
actual=n->a && n->a->sem_type ? n->a->sem_type : stored;
/* R8, from the M6 return case: a returned reference has to come from a
parameter or a static, never from a local. */
if (expected && fe_own_is_reference_like(expected) &&
!own_return_from_allowed_root(s,n->a))
err(s->c,n->loc,
"reference return must be derived from a parameter or static");
if (n->a && stored && stored->kind==FE_TYPE_VOID &&
expected && expected->kind!=FE_TYPE_VOID)
err(s->c,n->loc,"void expression returned from value function");
if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a &&
actual && actual->kind==FE_TYPE_ERROR_UNION &&
!fe_type_equal(expected,actual))
@@ -2625,17 +2668,11 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
break;
case FE_N_WHILE:
case FE_N_FOR:
/* One loop rule now that there is one checker: the body recurses
through this function, so any expression in it is checked the same
way whether or not the unit mentions optionals or error unions. */
if (n->kind==FE_N_WHILE) {
actual=check_expr(s,n->a);
if (known(actual) && actual->kind!=FE_TYPE_BOOL)
err(s->c,n->loc,"while condition must be bool");
++s->loop_depth;
check_stmt(s,n->b);
--s->loop_depth;
} else check_for(s,n);
/* The core loop case carries the flow capture and merge that detects a
value moved on every iteration, and it already recurses into the body
through this function, so there is nothing to special-case here. The
M7 half used to skip all of it. */
check_stmt_core(s,n);
break;
case FE_N_BREAK:
case FE_N_CONTINUE:
+22
View File
@@ -1580,6 +1580,14 @@ static void m7_emit_type_helpers(FeEmitter *e)
m7_c_type(e,t),t->full_slicer,m7_c_type(e,t),t->slicer);
fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n",
m7_c_type(e,t),t->tail_slicer,m7_c_type(e,t),t->slicer);
if (node_uses_trim(e->check->ast->root) && !t->ref_mut) {
fprintf(e->out,
"static %s fe_trim_%s(%s s) { unsigned long a=0; unsigned long b=s.n;"
" while (a<b && (s.p[a]==' '||s.p[a]=='\\t'||s.p[a]=='\\r'||s.p[a]=='\\n')) ++a;"
" while (b>a && (s.p[b-1]==' '||s.p[b-1]=='\\t'||s.p[b-1]=='\\r'||s.p[b-1]=='\\n')) --b;"
" return %s(s.p+a,b-a); }\n",
t->cname,t->cname,t->cname,t->maker);
}
}
}
}
@@ -1778,6 +1786,20 @@ static void m7_emit_call(FeEmitter *e, FeNode *n)
emit_m4_builtin(e,n);
return;
}
/* Every other builtin -- @size_of, @align_of and friends -- is lowered by
the core emitter. Without this the generic path below emits the call
verbatim, which is not C. */
if (!n->a && n->text && n->text[0]=='@') {
emit_expr_core(e,n);
return;
}
/* Same for the built-in alias methods on str: the core emitter knows how to
lower `line.trim()`, the generic member path would emit `.trim()`. */
if (n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text &&
strcmp(n->a->b->text,"trim")==0 && !n->children) {
emit_expr_core(e,n);
return;
}
if (n->a && n->a->kind==FE_N_MEMBER && n->a->a &&
n->a->a->kind==FE_N_IDENT && n->a->a->text &&
strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text &&