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 (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) {
if (base->kind==FE_TYPE_REF && !base->ref_mut) if (base->kind==FE_TYPE_REF && !base->ref_mut)
err(s->c,n->loc,"cannot write through shared reference"); 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); field=fe_type_field(owner,n->b->text);
if (!field) { if (!field) {
err(s->c,n->loc,"assignment requires a valid struct 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) && if (n->b && !fe_type_equal(expected,stored) &&
!m7_actual_compatible(expected,stored,n->b)) !m7_actual_compatible(expected,stored,n->b))
err(s->c,n->loc,"initializer type mismatch"); 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); if (n->b) mark_moved(s,n->b,actual);
initialized=n->b!=0; initialized=n->b!=0;
sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized, 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; sym->initialized=1;
fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc);
sym->moved=sym->own.move; 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; break;
case FE_N_EXPR_STMT: case FE_N_EXPR_STMT:
@@ -2614,6 +2648,15 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
else else
stored=fe_type_intern(&s->c->types,"void"); stored=fe_type_intern(&s->c->types,"void");
actual=n->a && n->a->sem_type ? n->a->sem_type : stored; 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 && if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a &&
actual && actual->kind==FE_TYPE_ERROR_UNION && actual && actual->kind==FE_TYPE_ERROR_UNION &&
!fe_type_equal(expected,actual)) !fe_type_equal(expected,actual))
@@ -2625,17 +2668,11 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
break; break;
case FE_N_WHILE: case FE_N_WHILE:
case FE_N_FOR: case FE_N_FOR:
/* One loop rule now that there is one checker: the body recurses /* The core loop case carries the flow capture and merge that detects a
through this function, so any expression in it is checked the same value moved on every iteration, and it already recurses into the body
way whether or not the unit mentions optionals or error unions. */ through this function, so there is nothing to special-case here. The
if (n->kind==FE_N_WHILE) { M7 half used to skip all of it. */
actual=check_expr(s,n->a); check_stmt_core(s,n);
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);
break; break;
case FE_N_BREAK: case FE_N_BREAK:
case FE_N_CONTINUE: 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); 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", 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); 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); emit_m4_builtin(e,n);
return; 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 && if (n->a && n->a->kind==FE_N_MEMBER && n->a->a &&
n->a->a->kind==FE_N_IDENT && n->a->a->text && 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 && strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text &&