From 730bcac282bbf579eb064faf216b338da371c03a Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:52:12 +0900 Subject: [PATCH] =?UTF-8?q?audit:=20SPEC-=ED=8C=8C=EC=84=9C=20=EA=B4=B4?= =?UTF-8?q?=EB=A6=AC=20=EC=9D=BC=EA=B3=B1=EC=9D=84=20=EC=A0=95=EB=A6=AC?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구현 셋, SPEC 다섯. 어느 쪽이 틀렸는지는 항목마다 따로 판정했다. 구현: - ~ 를 넣었다. ^ 는 xor 과 .^ 가 가져가서 비트 NOT 이 자기 철자를 못 갖고 있었다. 렉서·파서·검사·lowering(전부 1 과의 xor). - 전역 static/var 는 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 초기값의 생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. const 는 그대로 추론한다. - error code 는 정수 리터럴 하나다. 식이면 중복도 예약된 0 도 검사할 수 없다. SPEC: - 최상위 comptime if 를 뺐다. comptime 조건은 타입 술어뿐인데(§7.5) 유닛 바깥에는 바인딩된 타입 파라미터가 없어 물어볼 것이 없다. §11 v0.2. - 타입 이름은 [binding.]Name 이다. import 가 마지막 segment 를 바인딩하므로 점 둘 이상은 만들어질 수 없는데 문법이 unit_path 를 쓰고 있었다. - catch 의 EBNF 가 §4.6 보다 넓었다. 값을 주는 짧은 형태와 에러를 받는 블록 형태 둘로 나눠 적었다. - | 와 ^ 를 한 단계로 둔 것을 쪼갰다. 합치면 a | b ^ c 가 좌결합으로 (a|b)^c 가 되어 C 에서 온 사람을 속인다. 구현이 C 순서로 옳았다. - 마지막 필드 쉼표 생략을 명세에 적었다. enum 은 이미 허용하고 있었다. 그리고 tests/run.py 가 마커를 진단 스트림에만 맞춘다. --dump-ast 모드에서는 AST 덤프가 stdout 으로 먼저 나와서 parse/ fixture 는 마커를 쓸 수 없었다. 249/249, 39/39. --- AGENTS.md | 14 ++++++++ SPEC.md | 50 ++++++++++++++++++---------- audits/2026-08-17-spec-fec-parser.md | 19 +++++++++++ fec/src/checkexp.c | 5 +++ fec/src/lexer.c | 2 ++ fec/src/lexer.h | 2 +- fec/src/lowerexp.c | 7 ++++ fec/src/parser.c | 6 ++-- fec/tests/exec/bitnot.fe | 37 ++++++++++++++++++++ fec/tests/parse/badecode.fe | 5 +++ fec/tests/parse/badgtype.fe | 6 ++++ fec/tests/parse/okglobal.fe | 8 +++++ tests/run.py | 8 +++-- 13 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 fec/tests/exec/bitnot.fe create mode 100644 fec/tests/parse/badecode.fe create mode 100644 fec/tests/parse/badgtype.fe create mode 100644 fec/tests/parse/okglobal.fe diff --git a/AGENTS.md b/AGENTS.md index f881759..00e49bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ DOS/Windows용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. | `IR.md` | 중간 표현. 프론트엔드와 기계 사이 | | `TODO.md` | 남은 작업과 정해진 것. 언어 규칙은 `SPEC.md` 를 가리키기만 한다 | | `fec/tests/*/README.md` | 각 fixture 디렉터리가 무엇을 검사하는지 | +| `audits/<날짜>-<주제>.md` | 그때 조사해보니 어땠는지. 불변 기록 | ## 파이프라인 @@ -59,6 +60,19 @@ uv run python tests/build.py <프로그램.fe> # 하나만 빌드해서 돌려 줄 범위로 자르고 -- 주제별로 묶는 것보다 정확하다, 한 줄도 잃거나 겹치지 않으니 -- 공유하는 것은 비공개 헤더(`checkpri.h`, `lowerpri.h`)에 모은다. +## 조사 기록 + +한 번 조사하고 끝나는 것 -- 명세와 구현의 대조, 진단 증거 수집, 외부 감사 -- +은 `audits/<날짜>-<주제>.md`에 남긴다. 날짜와 기준 커밋을 적는다. 조사에서 +나온 **결론**은 `SPEC.md`나 `TODO.md`로 옮기고, audit 자체는 그때 무엇을 +봤는지의 기록으로 둔다. + +본문은 고치지 않는다. 다만 해결되면 맨 위에 **해결 줄 하나**를 붙인다 -- +어느 커밋에서 어떻게 정리됐는지. 그것 없이는 읽는 사람이 아직 살아있는 +문제인지 알 수 없다. + +계획 문서는 두지 않는다 -- 끝난 계획은 git log 다. + ## 작업 흐름 - 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신한다. 구현이 명세와 다르면 둘 중 하나가 diff --git a/SPEC.md b/SPEC.md index 13b0147..8716434 100644 --- a/SPEC.md +++ b/SPEC.md @@ -333,20 +333,21 @@ unit_path := ident ('.' ident)* import := 'import' unit_path ['as' ident] ';' decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl - | const_decl | global_decl) | comptime_decl -comptime_decl := 'comptime' 'if' expr '{' decl* '}' ['else' ('{' decl* '}' | comptime_decl)] + | const_decl | global_decl) fn_decl := ['extern' string] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type generic_params := '(' ident (',' ident)* ')' struct_decl := ['packed'] 'struct' ident [generic_params] '{' member* '}' + // field의 마지막 쉼표는 '}' 바로 앞에서 생략할 수 있다 member := ['pub'] (field | fn_decl) field := ident ':' type ',' enum_decl := 'enum' ident [generic_params] '{' variant (',' variant)* [','] '}' variant := ident | ident '(' type ')' | ident '{' vfield* '}' vfield := ident ':' type ',' -error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}' +error_decl := 'error' ident '{' ident '=' int_literal + (',' ident '=' int_literal)* [','] '}' const_decl := 'const' ident [':' type] '=' expr ';' global_decl := 'static' ident ':' type '=' expr ';' | 'var' ident ':' type '=' expr ';' @@ -378,21 +379,30 @@ pattern := ident // 배리언트, 페이로드 없음 | 'Some' '(' ident ')' | 'None' | int_literal | char_literal | 'true' | 'false' | '_' -qualified_name := ident ('.' ident)* -type := qualified_name - | '?' type | '!' type | qualified_name '!' type +type_name := [ident '.'] ident // [binding '.'] Name +type := type_name + | '?' type | '!' type | type_name '!' type | '^' type | '&' ['mut'] type | '*' type | '[' expr ']' type | '[' ']' ['mut'] type | 'fn' '(' [type (',' type)*] ')' ['->' type] - | qualified_name '(' type (',' type)* ')' // 제네릭 인스턴스 + | type_name '(' type (',' type)* ')' // 제네릭 인스턴스 -catch_expr := expr 'catch' ['|' ident '|'] (expr | block) +catch_expr := expr 'catch' expr | expr 'catch' '|' ident '|' block orelse_expr := expr 'orelse' expr ``` `member`의 `pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를 -섞어 쓸 수 있다. `catch`의 블록 형태는 값을 만들지 않으며 §4.6의 규칙을 따른다. -`unit_path` segment의 lexical 제한과 source path 대응은 §8.1이 규정한다. +섞어 쓸 수 있다. `catch`의 두 형태는 서로 다른 일을 한다: 값을 주는 짧은 형태와, +에러를 받아 빠져나가는 블록 형태다. 블록은 값을 만들지 않으므로(§11) 바인딩이 +있는 쪽만 블록을 받는다(§4.6). `unit_path` segment의 lexical 제한과 source path +대응은 §8.1이 규정한다. + +타입 이름은 `[binding '.'] Name`이다. `import`는 unit path의 **마지막 segment**를 +바인딩하므로(§8.2) 점이 둘 이상인 타입 이름은 만들어질 수 없다. `unit_path` +자체는 `import`와 `unit` 선언에서만 쓴다. + +전역 `static`/`var`는 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 초기값의 +생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. `const`는 추론한다. ### 6.2 표현식 우선순위 (낮음 → 높음) @@ -405,20 +415,23 @@ orelse_expr := expr 'orelse' expr 2 or 3 and 4 == != < <= > >= -5 | ^ -6 & -7 << >> -8 + - +% -% -9 * / % *% -10 단항: - not ~ & &mut try -11 후위: .field .? .^ [i] [a..b] (args) as T -12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) +5 | +6 ^ +7 & +8 << >> +9 + - +% -% +10 * / % *% +11 단항: - not ~ & &mut try +12 후위: .field .? .^ [i] [a..b] (args) as T +13 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) ``` - `and`, `or`는 단축 평가한다. 좌변이 답을 정하면 우변을 평가하지 않는다. - `orelse`와 `catch`도 lazy다. 좌변이 각각 `Some`/success이면 우변 또는 handler를 평가하지 않는다(§4.5·§4.6). - `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32`와 `-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다. +- `|`, `^`, `&`는 서로 다른 단계다. C와 같은 순서이며, 한 단계로 합치면 + `a | b ^ c`가 좌결합으로 `(a | b) ^ c`가 되어 C에서 온 사람을 속인다. - `..`는 일반 표현식 연산자가 아니며 `for` 헤더에서만 쓸 수 있다. - 비교 연산 체이닝 금지(`a < b < c`는 에러). - `.field`, `[i]`, `[a..b]`, 메서드 호출은 `&`, `&mut`, `^`를 필요한 만큼 자동 projection한다. 값 자체의 역참조는 `.^`가 필요하며 raw `*T`와 optional `?T`는 자동 역참조하지 않는다. @@ -791,6 +804,7 @@ binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다 | GC | 영구 | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 | | 암묵 형변환 | 영구 | 버그 원인 1위 | `as` | | 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 | +| 최상위 `comptime if` | v0.2 | 타깃이 하나이고 comptime 조건은 타입 술어뿐이라(§7.5) 유닛 바깥에는 물어볼 것이 없다 | 함수 안의 `comptime if`, 또는 유닛을 나눈다 | ### 11.1 인터페이스 설계 스케치 (v0.2 예정) diff --git a/audits/2026-08-17-spec-fec-parser.md b/audits/2026-08-17-spec-fec-parser.md index 0a406b4..933200e 100644 --- a/audits/2026-08-17-spec-fec-parser.md +++ b/audits/2026-08-17-spec-fec-parser.md @@ -3,6 +3,8 @@ - 날짜: 2026-08-17 - 기준 커밋: `52aaff62e490e37a0995aaaf7cbda47cf98e54a7` - 범위: `SPEC.md` §6과 `fec/src/lexer.c`, `fec/src/parser.c` +- **해결: 일곱 전부. 구현 셋(PARSE-04a·05·06), SPEC 다섯(01·02·03·04b·07).** + 판정과 근거는 아래 표에 덧붙였다. 본문은 조사 시점 그대로다. ## 현재 문제 @@ -16,6 +18,23 @@ | PARSE-06 | error code는 정수 literal | 일반 expression을 파싱하며 literal이 아니면 code 검증을 건너뜀 | `error E { Bad = 1 + 2, }`가 검사 통과 | | PARSE-07 | struct field와 enum vfield의 쉼표 필수 | 닫는 `}` 바로 앞에서는 쉼표 생략 허용 | `struct S { x: i32 }`가 검사 통과 | +## 해결 + +| ID | 어느 쪽이 틀렸나 | 무엇을 했나 | +|---|---|---| +| PARSE-01 | SPEC | comptime 조건은 타입 술어뿐이라(§7.5) 유닛 바깥에는 물어볼 것이 없다. `comptime_decl` 을 `decl` 에서 빼고 §11 v0.2 로 | +| PARSE-02 | SPEC | `import` 는 unit path 의 마지막 segment 를 바인딩하므로 점 둘 이상인 타입 이름은 만들어질 수 없다. 문법을 `type_name := [ident '.'] ident` 로 | +| PARSE-03 | SPEC | §4.6 과 §11(블록 표현식 배제)이 실제 규칙이고 EBNF 가 넓었다. 두 형태로 나눠 적었다 | +| PARSE-04a | 구현 | `~` 를 넣었다. 렉서·파서·검사·lowering(`xor` with all ones) | +| PARSE-04b | SPEC | `\|` 와 `^` 를 한 단계로 두면 `a \| b ^ c` 가 `(a\|b)^c` 가 되어 C 에서 온 사람을 속인다. 구현(C 순서)이 옳아서 표를 쪼갰다 | +| PARSE-05 | 구현 | 전역 `static`/`var` 는 타입 필수. `const` 는 그대로 추론 | +| PARSE-06 | 구현 | error code 는 정수 리터럴 하나만 받는다 | +| PARSE-07 | SPEC | 마지막 쉼표 생략은 흔하고 `enum` 은 이미 허용하고 있었다. 명세에 적었다 | + +fixture: `parse/badgtype.fe`, `parse/badecode.fe`, `parse/okglobal.fe`, +`exec/bitnot.fe`. 그리고 `tests/run.py` 가 마커를 진단 스트림에만 맞춘다 -- +`--dump-ast` 모드에서 AST 덤프가 먼저 나와 마커가 못 쓰이고 있었다. + ## 검증 - `uv run python tests/run.py`: `240/240` 통과 diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c index 1a32aa5..bf154a6 100644 --- a/fec/src/checkexp.c +++ b/fec/src/checkexp.c @@ -371,6 +371,11 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n) } else if (strcmp(op, "-") == 0) { if (known(a) && !fe_type_is_integer(a)) err(c, n->loc, "unary '-' requires integer"); + } else if (strcmp(op, "~") == 0) { + /* Flipping every bit only means something where the bits are the + value (SPEC 6.2). */ + if (known(a) && !fe_type_is_integer(a)) + err(c, n->loc, "unary '~' requires integer"); } else if (strcmp(op, "try") == 0) { /* SPEC 6.4: try is only allowed inside a function returning an error union. Checked on the expression rather than on the statement so diff --git a/fec/src/lexer.c b/fec/src/lexer.c index 2b25bfe..20866d7 100644 --- a/fec/src/lexer.c +++ b/fec/src/lexer.c @@ -145,6 +145,7 @@ FeToken fe_lexer_next(FeLexer *l) case '&': if(cur(l)=='&'){advance(l);fe_diag_error(l->diags,here(l,line,col),"&& is not a Ferro logical operator; use 'and'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_AND_EQ,start,line,col);} return tok(l,FE_TOK_AND,start,line,col); case '|': if(cur(l)=='|'){advance(l);fe_diag_error(l->diags,here(l,line,col),"|| is not a Ferro logical operator; use 'or'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_OR_EQ,start,line,col);} return tok(l,FE_TOK_OR,start,line,col); case '^': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_XOR_EQ,start,line,col);} return tok(l,FE_TOK_XOR,start,line,col); + case '~': return tok(l,FE_TOK_TILDE,start,line,col); default: fe_diag_error(l->diags,here(l,line,col),"unknown character"); return tok(l,FE_TOK_UNKNOWN,start,line,col); } } @@ -162,6 +163,7 @@ const char *fe_token_name(FeTokKind k) case FE_TOK_TRUE:return "true"; case FE_TOK_FALSE:return "false"; case FE_TOK_NULL:return "null"; case FE_TOK_UNDEFINED:return "undefined"; case FE_TOK_AND_KW:return "and"; case FE_TOK_OR_KW:return "or"; case FE_TOK_NOT:return "not"; case FE_TOK_BANG:return "!"; + case FE_TOK_TILDE:return "~"; case FE_TOK_LBRACE:return "{"; case FE_TOK_RBRACE:return "}"; case FE_TOK_LPAREN:return "("; case FE_TOK_RPAREN:return ")"; case FE_TOK_SEMI:return ";"; case FE_TOK_COLON:return ":"; case FE_TOK_COMMA:return ","; case FE_TOK_EQ:return "="; case FE_TOK_ARROW:return "->"; case FE_TOK_FATARROW:return "=>"; diff --git a/fec/src/lexer.h b/fec/src/lexer.h index 5c3bb47..c60c2ba 100644 --- a/fec/src/lexer.h +++ b/fec/src/lexer.h @@ -20,7 +20,7 @@ typedef enum FeTokKind { FE_TOK_PLUS_EQ, FE_TOK_MINUS_EQ, FE_TOK_STAR_EQ, FE_TOK_SLASH_EQ, FE_TOK_PERCENT_EQ, FE_TOK_PLUS_WRAP, FE_TOK_MINUS_WRAP, FE_TOK_STAR_WRAP, FE_TOK_EQ, FE_TOK_EQEQ, FE_TOK_NE, FE_TOK_LT, FE_TOK_LE, FE_TOK_GT, FE_TOK_GE, - FE_TOK_AND, FE_TOK_OR, FE_TOK_AND_KW, FE_TOK_OR_KW, FE_TOK_XOR, FE_TOK_NOT, FE_TOK_BANG, FE_TOK_SHL, FE_TOK_SHR, + FE_TOK_AND, FE_TOK_OR, FE_TOK_AND_KW, FE_TOK_OR_KW, FE_TOK_XOR, FE_TOK_TILDE, FE_TOK_NOT, FE_TOK_BANG, FE_TOK_SHL, FE_TOK_SHR, FE_TOK_AND_EQ, FE_TOK_OR_EQ, FE_TOK_XOR_EQ, FE_TOK_SHL_EQ, FE_TOK_SHR_EQ, FE_TOK_ANDAND, FE_TOK_OROR, FE_TOK_ARROW, FE_TOK_FATARROW, FE_TOK_AT, FE_TOK_QUESTION, FE_TOK_UNKNOWN diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c index e457fa7..ecaa36a 100644 --- a/fec/src/lowerexp.c +++ b/fec/src/lowerexp.c @@ -117,6 +117,13 @@ Slot lower_expr_core(Lower *L, FeNode *n) return slot_value(fe_ir_binary(L->m, L->b, FE_IR_SUB, it, zero, v, 0), it); } + if (n->text && !strcmp(n->text, "~")) { + /* Every bit flipped is every bit exchanged with a one. */ + unsigned ones = fe_ir_const(L->m, L->b, it, -1L); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_XOR, it, v, ones, + 0), it); + } if (n->text && !strcmp(n->text, "not")) { unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); unsigned v = as_value(L, lower_expr(L, n->a), n->a); diff --git a/fec/src/parser.c b/fec/src/parser.c index 888f5f6..836babf 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -167,7 +167,7 @@ static FeNode *postfix(FeParser *p) static FeNode *expr(FeParser *p, int minprec) { FeToken t=p->current; FeNode *left,*n; int prec; - if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); left=n; } + if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_TILDE)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); left=n; } else left=postfix(p); for(;;) { t=p->current;prec=precedence(t.kind);if(prec<=minprec)break;next(p);n=toknode(p,FE_N_BINARY,t);n->a=left;if(t.kind==FE_TOK_CATCH && eat(p,FE_TOK_OR)){if(is_name(p))n->b=toknode(p,FE_N_IDENT,p->current),next(p);else error(p,"expected catch binding");want(p,FE_TOK_OR,"expected '|' after catch binding");n->c=block(p);}else n->b=expr(p,prec);left=n; } return left; @@ -228,9 +228,9 @@ static FeNode *decl(FeParser *p) if(eat(p,FE_TOK_PACKED)) t=p->previous; if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(pub)n->flags|=FE_NODE_PUB;if(t.kind==FE_TOK_PACKED)n->flags|=FE_NODE_PACKED;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){int mpub=eat(p,FE_TOK_PUB);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,mpub,0,0,0));else fe_node_add(n,field(p,mpub));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } - if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } + if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");if(is(p,FE_TOK_INT)){v->a=toknode(p,FE_N_LITERAL,p->current);next(p);}else error(p,"an error code must be an integer literal");if(!is(p,FE_TOK_COMMA)&&!is(p,FE_TOK_RBRACE)){error(p,"an error code must be an integer literal");recover(p);break;}want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } - if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } + if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);else if(kk!=FE_TOK_CONST)error(p,"a global declaration requires an explicit type");want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } error(p,"expected declaration"); before=p->current.kind; recover(p); if (p->current.kind==before && p->current.kind!=FE_TOK_EOF) next(p); return 0; diff --git a/fec/tests/exec/bitnot.fe b/fec/tests/exec/bitnot.fe new file mode 100644 index 0000000..426b970 --- /dev/null +++ b/fec/tests/exec/bitnot.fe @@ -0,0 +1,37 @@ +// EXIT:0 +// OUTPUT:i32 -1 -6 4 +// OUTPUT:u8 255 250 +// OUTPUT:mask 240 15 +// OUTPUT:prec 7 3 +unit bitnot; + +import std.io; + +// `~` flips every bit (SPEC §6.2, 단항). `^` is taken by xor and by `.^`, so +// bitwise NOT needs its own spelling. +// +// `|`, `^` and `&` are three different levels, in C's order. Merging `|` and +// `^` would make `a | b ^ c` bind as `(a | b) ^ c`, which is not what anyone +// coming from C reads it as. + +fn main() -> i32 { + let a: i32 = 0; + let b: i32 = 5; + let c: i32 = -5; + @print("i32 {} {} {}\n", ~a, ~b, ~c); + + let u: u8 = 0; + let v: u8 = 5; + @print("u8 {} {}\n", (~u) as i32, (~v) as i32); + + // Clearing bits is what the operator is for. + let bits: u8 = 255; + let low: u8 = 15; + @print("mask {} {}\n", (bits & ~low) as i32, (bits & low) as i32); + + // 1 | 2 ^ 4 is 1 | (2 ^ 4) = 1 | 6 = 7, not (1 | 2) ^ 4 = 3 ^ 4 = 7. + // Those agree, so pick operands that do not: 3 | 1 ^ 2 is 3 | 3 = 3, + // while (3 | 1) ^ 2 would be 3 ^ 2 = 1. The 3 is the proof. + @print("prec {} {}\n", 1 | 2 ^ 4, 3 | 1 ^ 2); + return 0; +} diff --git a/fec/tests/parse/badecode.fe b/fec/tests/parse/badecode.fe new file mode 100644 index 0000000..eab8829 --- /dev/null +++ b/fec/tests/parse/badecode.fe @@ -0,0 +1,5 @@ +// ERROR:5:integer literal +unit badecode; + +// SPEC §6.1: error code 는 정수 리터럴이다. 식이면 중복도 예약된 0 도 검사할 수 없다. +error E { Bad = 1 + 2, } diff --git a/fec/tests/parse/badgtype.fe b/fec/tests/parse/badgtype.fe new file mode 100644 index 0000000..ed9c9a1 --- /dev/null +++ b/fec/tests/parse/badgtype.fe @@ -0,0 +1,6 @@ +// ERROR:6:requires an explicit type +unit badgtype; + +// SPEC §6.1: 전역은 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 +// 초기값의 생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. +static A = 1; diff --git a/fec/tests/parse/okglobal.fe b/fec/tests/parse/okglobal.fe new file mode 100644 index 0000000..02443a6 --- /dev/null +++ b/fec/tests/parse/okglobal.fe @@ -0,0 +1,8 @@ +unit okglobal; + +// `const` 는 여전히 추론한다 -- SPEC §6.1 이 타입을 요구하는 것은 전역 둘뿐이다. +const A = 1; +static B: i32 = 2; +var C: i32 = 3; + +error E { Bad = 1, Worse = 0x10, } diff --git a/tests/run.py b/tests/run.py index 34b3661..8cb5d84 100644 --- a/tests/run.py +++ b/tests/run.py @@ -82,6 +82,10 @@ def run_case(fec: Path, path: Path) -> tuple[bool, str]: f"--std={ROOT / 'fec'}"], capture_output=True, text=True, timeout=30) output = (done.stdout + done.stderr).strip() + # Diagnostics go to the diag stream. `--dump-ast` also writes the tree to + # stdout, so a marker matched against the two together would read the tree + # and never reach the error. + diags = done.stderr.strip() or output rejected = done.returncode != 0 if want.rejected != rejected: @@ -96,13 +100,13 @@ def run_case(fec: Path, path: Path) -> tuple[bool, str]: return True, "" # The marker pins where and roughly what, so a rule can be moved or reworded # only deliberately. - first = output.split("\n", 1)[0] if output else "" + first = diags.split("\n", 1)[0] if diags else "" at = re.search(r":(\d+):\d+: error:", first) if not at: return False, f"no diagnostic to match marker\n got: {first or '(silent)'}" if int(at.group(1)) != want.line: return False, f"marker says line {want.line}, diagnostic is line {at.group(1)}\n {first}" - if want.text and want.text.lower() not in output.lower(): + if want.text and want.text.lower() not in diags.lower(): return False, f"marker wants {want.text!r}\n {first}" return True, ""