콘텐츠로 이동

(KO) CUBRID Parser — Flex/Bison 파이프라인, PT_NODE 트리, 그리고 파서 메모리 모델

목차

SQL 파서는 두 개의 교과서적 문제 사이에 자리잡은 부품이다. 하나는 어휘 분석(lexical analysis) 이다. 평평한 문자 스트림을 문법이 다룰 수 있는 토큰 단위로 잘게 자르는 일이다. 다른 하나는 구문 분석(syntactic analysis) 이다. 토큰 스트림이 문맥 자유 문법(context-free grammar)에 부합하는지 판정하고, 부합한다면 그 유도 과정을 트리 형태로 보존하는 일이다. 두 문제 모두 Aho, Sethi, Lam, Ullman의 Compilers: Principles, Techniques, and ToolsDragon book — 이 표준 어휘를 굳혔으며, 그 핵심 추상은 현대 DBMS 파서에서도 변하지 않은 채로 살아 있다는 점이다. 어휘는 정규 언어 → DFA, 구문은 결정적 시프트-리듀스 자동기계(LR(1) / LALR(1) / GLR)다.

Database Internals(Petrov) 1장 §Query Processor는 같은 그림을 데이터베이스 쪽에서 정리한다. SQL 텍스트가 질의 처리기(query processor)에 들어오면, 파서는 내부 트리 표현을 만들어 내고, 옵티마이저가 그 트리를 재작성하고, 실행기가 재작성된 트리를 따라 간다는 흐름이다. 이 안에서 파서가 맡는 일은 단 하나 — 뒤따르는 패스들이 신뢰할 수 있는 트리를 만들어 내는 것이다. 그리고 지켜야 하는 제약도 단 하나다. 문법이 다루지 않는 입력을 조용히 받아들여서는 안 된다는 점이다.

렉서는 한 언어에 특화된 정규식 엔진이다. 모든 토큰 클래스 (키워드, 식별자, 정수 리터럴, 문자열 리터럴, 연산자)는 정규식으로 인코딩된다. 그 정규식들의 합집합이 단일 결정적 유한 자동기계(DFA)로 컴파일되며, DFA가 문자를 읽어 (token_class, lexeme) 쌍을 반환한다. Compilers §3은 NFA → DFA 변환을 유도한다. 실전 렉서에서 중요한 두 가지 포인트가 있다. 하나는 최장 일치 우선(longest match wins) 이라는 모호성 해소 규칙이다. 다른 하나는 동일한 표면 구문이 문맥에 따라 다른 의미를 갖도록 만드는 상태가 있는 시작 조건(start condition) 메커니즘이다. 예를 들어 문자열 내부 vs SQL 키워드 공간을 구분해야 하는 상황 말이다.

Flex는 Lex의 GNU 구현이다. 호출 한 번에 토큰 하나를 반환하는 C 함수 yylex()를 생성하며, yytext, yyleng, 그리고 파서가 reduce 시점에 사용할 토큰별 값 슬롯 yylval을 외부에 노출한다. CUBRID의 렉서는 src/parser/csql_lexer.l에 있다. 빌드가 실제로 컴파일하는 것은 거기서 생성된 csql_lexer.c다.

Bison은 의미 액션이 붙은 문맥 자유 문법으로부터 시프트-리듀스 파서를 생성한다. 기본 형태는 LALR(1) 이다. 토큰 한 개의 선조사(lookahead)를 사용하는 LR이며, Compilers §4.7에서 다루는 자동기계 패밀리다. LALR(1)은 대부분의 프로그래밍 언어 문법은 깔끔하게 다룬다. 다만 SQL은 깔끔하게 다루지 못한다. SQL에는 역사적 모호성이 박혀 있기 때문이다. 고전적 예가 SELECT a FROM b GROUP BY c, d이다. 여기서 c, d는 방언에 따라 리스트일 수도 있고 튜플일 수도 있다.

Bison의 비상구가 GLR(Generalized LR)이다. LALR(1) 자동기계가 충돌을 만나면 분기(fork)를 일으켜 양쪽 가지를 병렬로 파싱하고, reduce에 실패한 가지를 폐기하는 방식이다. 성능 비용은 실재하지만 유한하다. 실제 SQL 질의는 충돌 지점이 몇 군데에 불과하기 때문이다. CUBRID는 문법을 %glr-parser로 컴파일한다 (csql_grammar.y%glr-parser 선언). 이 선택 덕분에 표면 SQL이 허용하는 MySQL/Oracle 조인의 합집합, 힌트 구문, 메서드 호출 구문 등을 한 문법으로 받아들일 수 있다.

Bison의 의미 액션 메커니즘이 트리를 만드는 본체다. 모든 문법 규칙에는 C 액션 블록이 박혀 있다. 파서가 그 규칙으로 reduce할 때 액션이 실행된다. 액션은 우변의 의미값을 $1, $2, ...로 보고, 규칙 자신의 값을 $$에 쓴다. 그 값들의 타입은 %union 선언이 Bison에 알려 준다. CUBRID에서 이 유니언은 PT_NODE *node, int number, char *cptr, 그리고 몇 가지 헬퍼 컨테이너를 담는다. 사용자 입력을 파스 트리 노드가 만들어지는 유일한 장소가 바로 이 문법이다. 그 외의 모듈은 모두 문법이 만들어 둔 것을 읽기만 할 뿐이다.

파스 트리는 파서가 시스템 나머지에 넘기는 계약이다. 모든 구체 구현이 마주치는 두 설계 결정이 있다.

  1. 다형성 메커니즘. 모든 파스 트리는 모양이 제각각인 노드를 가진다는 점이다 (SELECTWHERE 절이 아니고, WHERE 절은 리터럴이 아니다). 지배적인 세 가지 접근이 있다.

    • 클래스 계층(현대 C++ / Java): 베이스 클래스 하나에 가상 메서드. 관습적이지만 RTTI와 가상 디스패치를 요구한다.
    • 태그 유니언(C 방언): 판별 태그 하나와 타입별 서브구조의 union이 한 struct 안에 들어 있다. 런타임 비용은 싸지만 컴파일 타임 안전성은 약해진다.
    • 합 타입 ADT(Haskell / OCaml / Rust): 언어 자체의 유니언 타입과 망라적(exhaustive) 패턴 매칭.

    PostgreSQL, MySQL, CUBRID는 모두 같은 C 시대적 이유로 태그 유니언 쪽을 골랐다.

  2. 오류 회복. Compilers §4.1.4는 네 가지 고전 전략을 열거한다. panic-mode(동기화 토큰을 만날 때까지 건너뛰기), 구문 단위 회복 (rule이 reduce되도록 토큰을 갈아끼우기), 오류 생성 규칙(잘못된 입력을 매칭하는 문법 규칙), 그리고 전역 보정(편집 거리가 최소가 되는 유효 프로그램 찾기)이다. Bison의 내장 error 토큰은 panic-mode다. 실제 엔진은 그 위에 위치 추적과 메시지 합성을 직접 얹는다.

CUBRID의 조합은 관습적이다. 시작 조건이 있는 Flex, %glr-parser Bison, 태그 유니언 PT_NODE, 그리고 panic-mode 회복에 YYLTYPE 위치 정보를 더한 형태다. 이 위치 정보가 원본 버퍼의 어느 바이트가 문제인지 정확히 짚어 준다. 차별화된 공학적 선택은 메모리 모델노드 타입별 자식 레이아웃에 있다. 두 가지 모두 §CUBRID의 구현 에서 따라간다.

클라이언트 측에서 SQL을 컴파일하는 모든 SQL 엔진은 같은 일감 세트를 마주한다. PostgreSQL, MySQL, CUBRID에 걸쳐 정착한 공학적 어휘가 이 섹션의 렌즈다. §CUBRID의 구현에 등장하는 CUBRID 고유 코드는 이 공유 설계 공간 위에서 어느 다이얼을 어떻게 돌렸는가로 읽는 것이 적합하다.

기본 프런트엔드 스택으로서의 Flex + Bison

섹션 제목: “기본 프런트엔드 스택으로서의 Flex + Bison”

가장 자주 인용되는 C/C++ RDBMS 셋 — PostgreSQL, MySQL, CUBRID — 은 모두 Flex/Bison 소스에서 렉서와 파서를 생성한다. PostgreSQL의 파일은 src/backend/parser/scan.lsrc/backend/parser/gram.y다. MySQL은 sql/sql_lex.ccsql/sql_yacc.yy다. CUBRID는 src/parser/csql_lexer.lsrc/parser/csql_grammar.y다. 생성 단계는 빌드의 일부이며 (CUBRID는 CMake, Postgres는 Autoconf), 생성된 .c 파일은 프로젝트마다 체크인 정책이 다르다. CUBRID는 입력만 체크인한다.

공유되는 모양은 다음과 같다.

  • 세 섹션(Definitions, Rules, User subroutines) 구조의 .l 파일 하나. Rules 섹션은 토큰 코드를 반환하며, 그 코드는 문법의 %token 선언과 일치한다.
  • %union, %type, %token, 시작 심볼, 각 규칙에 C 액션 블록이 매달린 본체, 그리고 액션 안에서 호출되는 헬퍼 함수가 들어가는 에필로그를 갖춘 .y 파일 하나.
  • CMake의 flex_target / bison_target으로 빌드에 결선된 생성 산출물.

ParseNode / PT_NODE / Item — 태그 유니언 AST

섹션 제목: “ParseNode / PT_NODE / Item — 태그 유니언 AST”

PostgreSQL의 Node(src/include/nodes/nodes.h)는 모든 파스 트리, 플랜 트리, 실행기 트리 원소에 NodeTag를 매단다. 그 태그가 포인터가 실제로 가리키는 구체 struct를 결정하며, 방문자 스타일의 순회는 태그 위에서 switch한다.

MySQL은 표면을 다르게 가른다. 표현식은 Item, 질의 블록은 SELECT_LEX 등 — 다만 각 변종은 태그를 들고 공통 베이스 타입을 공유한다. MySQL의 경우 그 판별자가 클래스 정체성이다(C++ 상속을 실제로 쓰는 쪽이다).

CUBRID의 PT_NODE는 Postgres의 Node와 같은 발상이되 상속이 빠진 형태다. 단일 struct 안에 PT_NODE_TYPE node_type 필드 하나, 그리고 node_type에 따라 레이아웃이 달라지는 거대한 union pt_statement_info info 필드 하나가 들어 있다. 모든 타입별 서브구조는 PT_<TYPE>_INFO 이름을 가지며, node->info.<lowercase> 로 접근된다.

파스 시점 할당은 짧게 살고, 자주 일어난다. 모든 엔진이 어떤 형태로든 아레나(arena) / 영역(region) / 존(zone) 할당자를 쓴다. 큰 블록 몇 개를 미리 잡아 놓고, 노드 하나하나는 포인터를 한 칸씩 밀어 가면서 잘라 내며, 마지막에 아레나 전체를 단 한 번 free로 해제하는 방식이다. PostgreSQL은 MemoryContext를 갖는다 (컨텍스트 단위 palloc/pfree). MySQL은 MEM_ROOT다. CUBRID는 PARSER_CONTEXT와 그에 매단 파서 단위 PARSER_NODE_BLOCK / PARSER_STRING_BLOCK 풀을 parse_tree.c에 둔다.

세 엔진의 이득은 동일하다. 파싱 도중에 만들어졌으나 결과적으로 도달 불가능해진 노드들 — 어떤 규칙이 reduce에 실패했거나, 의미 검사가 다시 작성해 버린 경우 — 은 개별 free가 필요 없다는 점이다. 파서 컨텍스트와 함께 통째로 사라지기 때문이다. 비용도 동일하다. 노드 포인터는 파서보다 오래 살 수 없다. 파서가 닫힌 뒤에도 옵티마이저나 실행기가 노드를 필요로 한다면 자기 아레나로 복사해야 한다는 점이다. PostgreSQL의 copyObject가 맡는 역할이고, CUBRID의 parser_copy_tree가 맡는 역할이다.

노드 타입별 자식 레이아웃 — apply 함수 표

섹션 제목: “노드 타입별 자식 레이아웃 — apply 함수 표”

파스 트리 워커는 각 노드 타입이 어떤 자식을 갖는지 알아야 한다. 세 가지 접근이 있다.

  • 워커마다 노드 타입별 switch를 직접 박는 방식. 빠르지만 망가뜨리기 쉽다.
  • 리플렉션(Java, C#) — 런타임에 필드 타입을 읽는다. 느리고 빌드 타임이 무거워진다.
  • 판별자로 인덱싱하는 함수 포인터 표 — 타입마다 손으로 작성한 함수 하나가 그 타입의 올바른 자식 집합을 방문한다.

Postgres는 매크로가 visitor 코드로 펼쳐지는 형태로 셋째 접근을 쓴다. CUBRID는 더 명시적으로, PT_NODE_TYPE마다 함수가 들어 있는 pt_apply_f 배열을 둔다. 초기화 / pretty print용으로 평행하게 놓인 pt_init_f / pt_print_f 배열도 함께 있다. 트레이드오프는 이렇다. 새 노드 타입을 추가할 때마다 세 함수를 등록해야 한다는 약간의 빌드 타임 세금을 내고, 빠르고 디버깅하기 쉬운 워커를 얻는다.

이론적 / 공통 개념CUBRID 명칭
렉서 소스src/parser/csql_lexer.l (flex)
생성된 렉서csql_lexer.c (CMake flex_target이 생성)
문법 소스src/parser/csql_grammar.y (bison %glr-parser)
생성된 파서csql_grammar.c (bison_target이 생성)
토큰 타입 prefixcsql_yy* (--name-prefix=csql_yy / --prefix=csql_yy로 설정)
렉서 진입점csql_yylex (Bison 생성 csql_yyparse가 호출)
파서 진입점csql_yyparseparser_main이 감쌈 (csql_grammar.y)
파스 트리 노드PT_NODE (parse_tree.h)
노드 판별자enum pt_node_type PT_NODE_TYPE (parse_tree.h)
타입별 서브구조 유니언union pt_statement_info PT_STATEMENT_INFO (parse_tree.h)
다형 자식 방문 표pt_apply_f[PT_NODE_NUMBER] (parse_tree_cl.c)
다형 init 표pt_init_f[PT_NODE_NUMBER]
다형 print 표pt_print_f[PT_NODE_NUMBER]
트리 워커parser_walk_tree, parser_walk_leaves (parse_tree_cl.c)
파서 단위 아레나PARSER_CONTEXT + PARSER_NODE_BLOCK + PARSER_STRING_BLOCK (parse_tree.c)
노드 할당자parser_new_nodeparser_create_node → 블록 bump (parse_tree_cl.c, parse_tree.c)
문자열 할당자parser_alloc (parse_tree.c)
렉서 토큰 이름 인터닝pt_makename (scanner_support.c)
Bison 위치 타입YYLTYPE { first_line, first_column, last_line, last_column, buffer_pos } (csql_grammar.y)
노드별 line/columnPT_NODE::line_number, column_number, buffer_pos
오류 토큰Bison 내장 error; 메시지는 csql_yyerrorPT_ERRORmf
파서 컨텍스트 라이프사이클parser_create_parser / parser_free_parser (parse_tree.c)
공개 문자열 파스 진입점parser_parse_string, parser_parse_string_with_escapes (parse_tree_cl.c)

세 단계를 차례로 본다. (1) 런타임의 렉스 → 파스 → 트리 파이프라인, (2) 그 파이프라인이 만들어 내는 PT_NODE 데이터 모델, (3) 생산자와 소비자가 노드 포인터를 안전하게 공유하도록 만드는 메모리 모델.

flowchart LR
  SQL["SQL 텍스트"]
  PC["PARSER_CONTEXT"]
  PARSE["parser_parse_string"]
  MAIN["parser_main"]
  LEX["csql_yylex<br/>(csql_lexer.l에서 생성)"]
  PARSER["csql_yyparse<br/>(csql_grammar.y에서 생성)"]
  ACT["reduce 액션<br/>parser_new_node / parser_make_∗"]
  POOL["PARSER_NODE_BLOCK 풀<br/>(파서 단위 아레나)"]
  STK["parser->node_stack<br/>(최상위 statement 리스트)"]
  TREE["PT_NODE 루트 트리"]
  STMTS["parser->statements[]"]

  SQL --> PARSE
  PARSE --> PC
  PARSE --> MAIN
  MAIN --> LEX
  MAIN --> PARSER
  LEX -- "(token, csql_yylval)" --> PARSER
  PARSER -- "reduce 마다" --> ACT
  ACT -- "아레나에서 잘라냄" --> POOL
  ACT -- "자식 결선" --> TREE
  PARSER -- "최상위 reduce" --> STK
  MAIN -- "스택 스냅샷" --> STMTS
  STMTS --> TREE

이 그림은 세 경계를 명시한다. (문법 / 할당자) 문법 규칙이 parser_new_node를 호출하는 유일한 장소다. 노드는 reduce 액션에서 태어난다. (파스 / 드라이브) parser_mainyyparse를 감싸는 얇은 래퍼다. 호출 단위 셋업(라인/컬럼 리셋, 버퍼 위치 저장/복원, 힌트 표 초기화, 최상위 statement 배열 스냅샷)을 책임진다. (호출 단위 아레나) 호출 도중 할당된 모든 노드와 문자열은 PARSER_CONTEXT가 소유한 블록에 살며, parser_free_parser에 의해 일괄 해제된다. 힙에 개별로 존재하는 것은 없다는 점이다.

Flex 소스는 표준 3섹션 레이아웃을 따른다.

// csql_lexer.l (skeleton)
%{
#include "csql_grammar.h"
#include "csql_grammar_scan.h"
#include "parse_tree.h"
#include "parser_message.h"
#include "system_parameter.h"
#include "message_catalog.h"
#define CSQL_MAXNAME 256
static int parser_yyinput (char *buff, int max_size);
#undef YY_INPUT
#define YY_INPUT(buffer, result, max_size) { \
result = parser_yyinput(buffer, max_size); \
result == 0 ? result = YY_NULL : result; \
}
#define YY_USER_ACTION { \
yybuffer_pos += csql_yyget_leng (); \
csql_yylloc.buffer_pos = yybuffer_pos; \
}
%}
%option yylineno
%x QUOTED_CHAR_STRING DOUBLY_QUOTED_CHAR_STRING DELIMITED_ID_NAME
%x BRACKET_ID_NAME BACKTICK_ID_NAME PLCSQL_TEXT
/* ... twelve more start conditions ... */
%%
[sS][eE][lL][eE][cC][tT] { begin_token(yytext); return SELECT; }
[sS][eE][nN][sS][iI][tT][iI][vV][eE]
{ begin_token(yytext); return SENSITIVE; }
[sS][eE][pP][aA][rR][aA][tT][oO][rR]
{ begin_token(yytext);
csql_yylval.cptr = pt_makename(yytext);
return SEPARATOR; }
/* ... ~2000 more rules ... */
%%
/* user subroutines: parser_yyinput, csql_yyerror, yywrap, ... */

이 코드 조각에는 의도적인 선택 셋이 들어 있다.

  1. YY_INPUT이 재정의되어 있다. Flex 기본 YY_INPUTstdin에서 읽는다. CUBRID는 인메모리 버퍼(parser->buffer)를 파싱하므로 Flex 입력을 parser_yyinput로 우회시킨다. 이 함수는 파서 컨텍스트 소유의 버퍼에서 바이트를 복사해 오면서 yybuffer_pos를 추적한다.
  2. YY_USER_ACTION이 바이트 위치 카운터를 누적한다. yybuffer_pos는 Flex가 방금 매칭한 토큰의 원본 SQL 문자열 안 누적 오프셋이다. 이후 모든 오류 메시지와 모든 PT_NODE::buffer_pos 가 이 값을 기준점으로 삼는다는 점이다. 줄/컬럼만으로는 충분치 않다. 오류 메시지가 문제 지점의 부분 문자열을 인용해야 하기 때문에 바이트 오프셋이 필요하다는 점이다.
  3. 키워드 규칙은 문자 클래스 나열로 대소문자 무관하게 매칭한다. [sS][eE][lL][eE][cC][tT]는 Flex의 %option case-insensitive 를 켜지 않고 대소문자 무관 매칭을 얻기 위한 수동 방식이다. 그 옵션을 켜면 식별자 규칙과 문자열 리터럴 규칙에도 같이 영향을 미치기 때문이다. 생성 DFA는 키워드마다 약간의 상태를 더 쓰지만, 대안(입력을 먼저 소문자화)은 FooBar 같은 구분 식별자(delimited identifier)를 망가뜨린다는 점이다.

User subroutines 블록에는 입력 드라이버 (parser_yyinput, parser_yyinput_single_line, parser_yyinput_multi_line), 오류 보고기 (csql_yyerror / csql_yyerror_explicit), 그리고 입력 끝 핸들러 yywrap가 들어 있다. yywrap은 항상 1을 반환한다. 파서는 parser_parse_string 호출당 단일 버퍼만 보며, 체인된 입력을 만지지 않는다는 점이다.

열네 개의 시작 조건 중 발췌(QUOTED_CHAR_STRING, DOUBLY_QUOTED_CHAR_STRING, DELIMITED_ID_NAME, BRACKET_ID_NAME, BACKTICK_ID_NAME, PL_LANG_SPEC, PLCSQL_TEXT, …)는 동일한 표면 문자(예를 들어 ")가 문자열 리터럴 내부와 구분 식별자 시작 지점에서 서로 다른 의미를 갖는 사실을 처리하는 방식이다. 문법/렉서 안의 BEGIN(state) 액션이 매번 렉서를 그 문맥에서만 유효한 규칙 들로만 매칭이 가능한 상태로 전환한다. PL/CSQL 텍스트는 멀티라인 균형 BEGIN ... END 블록을 매칭한다. expecting_pl_lang_spec은 문법이 프로시저 본체를 보기 직전에 세워 두는 일회용 플래그다.

문법은 SQL의 모호성을 처리하기 위해 Bison %glr-parser를 사용한다. %union은 reduce 액션이 반환할 수 있는 값 타입을 들고 있고, 규칙은 PT_NODE를 만들어 낸다.

// csql_grammar.y — declarations (excerpt)
%{/*%CODE_REQUIRES_START%*/
#include "json_table_def.h"
#include "parser.h"
typedef struct YYLTYPE
{
int first_line;
int first_column;
int last_line;
int last_column;
int buffer_pos; /* cumulative byte offset; updated by YY_USER_ACTION */
} YYLTYPE;
#define YYLTYPE_IS_DECLARED 1
/*%CODE_END%*/%}
%locations
%glr-parser
%define parse.error verbose
%union
{
int number;
bool boolean;
PT_NODE *node;
char *cptr;
container_2 c2;
container_3 c3;
container_4 c4;
container_10 c10;
container_11 c11;
struct json_table_column_behavior jtcb;
}
%type <node> stmt
%type <node> select_stmt
%type <node> select_expression
%type <node> select_expression_opt_with
/* ... ~1100 more %type lines ... */

YYLTYPE 확장은 렉서의 buffer_pos를 reduce를 통과하면서도 보존하기 위한 장치다. Bison 기본 YYLTYPE(first_line, first_column, last_line, last_column)만 들고 있다. CUBRID는 buffer_pos를 더해, 오류 메시지가 원본 SQL 텍스트에서 문제 바이트 범위를 잘라 보여 줄 수 있게 한다.

opt_with_clauseselect_expression을 짝지어 주는, 대표적인 reduce 액션의 형태는 다음과 같다.

// csql_grammar.y — select_expression_opt_with
select_expression_opt_with
: opt_with_clause
select_expression
{{
PT_NODE *with_clause = $1;
PT_NODE *stmt = $2;
if (stmt && with_clause)
{
stmt->info.query.with = with_clause;
}
$$ = stmt;
}}
;

세 가지를 짚어 둘 만하다. (a) 액션은 결선만 한다. 안쪽 규칙이 이미 반환한 PT_NODE를 잇는 일이다. PT_SELECT 자체는 훨씬 안쪽의 액션, 즉 select_stmtparser_new_node (this_parser, PT_SELECT)에서 할당되었다. (b) 액션은 return하지 않는다. 생성된 파서의 지역 변수 $$에 쓴다. (c) this_parser는 파일 정적 PARSER_CONTEXT *다. parser_mainyyparse 호출 직전에 세팅하고 호출이 끝나면 원복한다는 점이다.

실제 PT_SELECT 할당은 다음 위치에서 일어난다.

// csql_grammar.y — select_stmt (excerpt)
select_stmt
: SELECT
{{
PT_NODE *node;
parser_save_found_Oracle_outer ();
if (parser_select_level >= 0)
parser_select_level++;
parser_hidden_incr_list = NULL;
node = parser_new_node (this_parser, PT_SELECT);
if (node)
{
node->info.query.q.select.flavor = PT_USER_SELECT;
node->info.query.q.select.hint = PT_HINT_NONE;
}
parser_push_select_stmt_node (node);
parser_push_hint_node (node);
}}
opt_hint_list /* $3 */
all_distinct /* $4 */
select_list /* $5 */
opt_select_param_list /* $6 */
/* ... wire children, pop the stmt node, set $$, ... */
;

문법 전반에 걸쳐 반복되는 두 패턴이 여기에 있다. 노드를 생성하는 액션에서 일어나는 statement 스택 push, 그리고 더 뒤의 액션에서 일어나는, 노드를 마무리짓는 statement 스택 pop이다. 이 구조 덕분에 opt_hint_list 같은 중간 규칙(mid-rule action)은 자기 자신이 어떤 PT_SELECT에 매달리는지 명시 인자 없이도 알 수 있다는 점이다.

parser_maincsql_grammar.y 맨 아래에 산다(거기 있어야만 한다. yyparse가 생성된 csql_grammar.c 안에서 static 함수로 정의되기 때문에 .y 파일 바깥에서 호출하기가 불편하기 때문이다).

// parser_main — csql_grammar.y (condensed)
PT_NODE **
parser_main (PARSER_CONTEXT * parser)
{
long desc_index = 0;
long i, top;
int rv, yybuffer_pos_save;
PARSER_CONTEXT *this_parser_saved;
if (!parser)
return 0;
parser_output_host_index = parser_input_host_index = desc_index = 0;
this_parser_saved = this_parser;
this_parser = parser; /* publish to grammar-action TLS */
dbcs_start_input ();
yycolumn = yycolumn_end = 1;
yybuffer_pos_save = yybuffer_pos;
yybuffer_pos = 0;
is_dblink_query_string = 0;
expecting_pl_lang_spec = 0;
csql_yylloc.buffer_pos = 0;
g_query_string = NULL;
g_query_string_pos = 0;
g_query_string_len = 0;
g_original_buffer_len = 0;
pt_initialize_hint (parser, parser_hint_table);
rv = yyparse (); /* drives csql_yylex through tokens */
/* parser_main can be re-entered (loaddb -s); restore yybuffer_pos. */
yybuffer_pos = yybuffer_pos_save;
pt_cleanup_hint (parser, parser_hint_table);
if (pt_has_error (parser) || parser->stack_top <= 0 || !parser->node_stack)
{
parser->statements = NULL;
}
else
{
parser->statements = (PT_NODE **)
parser_alloc (parser, (1 + parser->stack_top) * sizeof (PT_NODE *));
if (parser->statements)
{
for (i = 0, top = parser->stack_top; i < top; i++)
parser->statements[i] = parser->node_stack[i];
parser->statements[top] = NULL;
}
parser->host_var_count = parser_input_host_index;
/* ... allocate host_variables, host_var_expected_domains ... */
}
/* ... restore this_parser ... */
return parser->statements;
}

this_parseryybuffer_pos를 입출구에서 저장/복원하는 이유는 loaddb -s의 재진입 경로 때문이다. loaddb 안의 파스-실행 루프가 바깥쪽 parser_main이 입력 파일의 statement를 파싱하고 있는 동안 parser_main을 재귀적으로 호출할 수 있다는 점이다.

parser->statements 배열은 NULL 종결 최상위 statement 리스트다. CUBRID 파서는 다중 statement 버퍼를 받을 수 있다(예: SELECT 1; SELECT 2;). 최상위 규칙이 reduce될 때마다 각 statement를 node_stack에 push하고, parser_main이 끝에 가서 그 스택을 결과 배열로 복사한다.

PT_NODE — 다형 태그 유니언 노드

섹션 제목: “PT_NODE — 다형 태그 유니언 노드”

노드 타입은 한 struct, 한 태그, 한 유니언이다.

// parser_node — src/parser/parse_tree.h
struct parser_node
{
PT_NODE_TYPE node_type; /* the type of SQL statement this represents */
int parser_id; /* which parser did I come from */
int line_number; /* the user line number originating this */
int column_number; /* the user column number originating this */
int buffer_pos; /* position in the parse buffer of the string */
char *sql_user_text; /* user input sql string */
int sql_user_text_len;
PT_NODE *next; /* forward link for NULL-terminated list */
PT_NODE *or_next; /* forward link for DNF list */
PT_NODE *next_row; /* PT_VALUE/NAME/EXPR row in PT_NODE_LIST */
void *etc; /* application-specific info hook */
UINTPTR spec_ident; /* entity-spec equivalence class (for FROM resolve) */
TP_DOMAIN *expected_domain; /* expected domain for input marker */
PT_NODE *data_type; /* for non-primitive types, sets, objects */
XASL_ID *xasl_id; /* XASL_ID for this SQL statement */
const char *alias_print;
PARSER_VARCHAR *expr_before_const_folding;
PT_TYPE_ENUM type_enum; /* PT_TYPE_INTEGER / VARCHAR / ... */
CACHE_TIME cache_time;
int sub_host_var_count;
int *sub_host_var_index;
struct { /* 27 single-bit flags */
unsigned recompile:1;
unsigned cannot_prepare:1;
/* ... use_plan_cache, is_hidden_column, with_rollup,
* do_not_fold, is_value_query, do_not_replace_orderby,
* use_auto_commit, ... 27 in total */
} flag;
PT_STATEMENT_INFO info; /* depends on 'node_type' field */
};

세 가지를 짚는다.

  • next / or_next / next_row은 서로 독립인 세 개의 리스트 스파인이다. next는 자연스러운 리스트(select 리스트의 원소, AND로 묶인 술어, FROM 리스트의 spec)다. or_next는 술어 정규화를 위한 DNF 스파인이다(a = 1 OR a = 2 OR a = 3next 하나에 or_next 셋이 매달린 형태가 된다). next_rowPT_NODE_LIST로 reduce되는 VALUES 질의를 위한 것이다. 워커 (pt_walk_private)는 각 스파인을 독립적으로 내려간다. 변환 패스가 한 스파인에 끼어들어도 다른 스파인을 흔들지 않게 만들어 주는 구조다.
  • data_type은 자식이지 인-플레이스 타입 태그가 아니다. type_enum은 싼 판별자다(PT_TYPE_INTEGER, PT_TYPE_VARCHAR). data_type별도의 PT_DATA_TYPE 타입 PT_NODE 다. 도메인 특화 부가 정보(precision, scale, collation)를 들고 있다. 워커가 부르는 PT_APPLY_WALK (parser, node->data_type, walk) 덕분에 타입 검사 변환이 부모 종류에 무관하게 균일해진다.
  • line / column / buffer_pos가 모든 노드에 산다. parser_new_node 안에서 pt_parser_line_col이 채워 넣는다. 그 덕분에 어떤 후속 패스든 — 의미 검사, 옵티마이저, 심지어 XASL 생성까지 — 다시 파싱하지 않고도 노드의 소스 범위를 인용한 진단을 내놓을 수 있다.

PT_NODE_TYPE enum은 이중 의미를 들고 있다. SQL statement에 대한 값들(PT_SELECT, PT_INSERT, …)은 의도적으로 API의 CUBRID_STMT_* 코드와 같은 정수다. 그래서 PT_NODE 판별자를 변환 없이 API 클라이언트에 돌려줄 수 있다는 점이다.

// PT_NODE_TYPE — src/parser/parse_tree.h (excerpt)
enum pt_node_type
{
PT_NODE_NONE = CUBRID_STMT_NONE,
PT_ALTER = CUBRID_STMT_ALTER_CLASS,
PT_CREATE_ENTITY = CUBRID_STMT_CREATE_CLASS,
PT_INSERT = CUBRID_STMT_INSERT,
PT_SELECT = CUBRID_STMT_SELECT,
PT_UPDATE = CUBRID_STMT_UPDATE,
PT_DELETE = CUBRID_STMT_DELETE,
/* ... ~50 more statement-typed nodes ... */
PT_DIFFERENCE = CUBRID_MAX_STMT_TYPE, /* internal-only types from here down */
PT_INTERSECTION,
PT_UNION,
/* expression / clause / fragment nodes */
PT_EXPR, PT_NAME, PT_VALUE, PT_FUNCTION, PT_DOT_,
PT_SPEC, PT_DATA_TYPE, PT_SORT_SPEC, PT_HOST_VAR,
/* ... ~40 more ... */
PT_NODE_NUMBER, /* the number of node types */
PT_LAST_NODE_NUMBER = PT_NODE_NUMBER
};

이 분기는 유의미하다. node_type < CUBRID_MAX_STMT_TYPE은 API와 실행기 양쪽이 모두 필요로 하는 최상위 statement를 가리킨다. node_type >= CUBRID_MAX_STMT_TYPE은 내부 파스 트리 기계 부품이라는 점이다.

타입별 info 유니언은 parse_tree.h:3470에 있다.

// pt_statement_info — src/parser/parse_tree.h (excerpt)
union pt_statement_info
{
PT_ZZ_ERROR_MSG_INFO error_msg;
PT_ALTER_INFO alter;
PT_ALTER_TRIGGER_INFO alter_trigger;
PT_CREATE_ENTITY_INFO create_entity;
PT_DELETE_INFO delete_;
PT_DOT_INFO dot;
PT_EXPR_INFO expr;
PT_FUNCTION_INFO function;
PT_INSERT_INFO insert;
PT_NAME_INFO name;
PT_QUERY_INFO query; /* SELECT, UNION, etc. */
PT_SPEC_INFO spec; /* FROM-clause table spec */
PT_VALUE_INFO value;
PT_DATA_TYPE_INFO data_type;
/* ... 80+ more variants ... */
};

대부분의 사용자가 파스 트리를 떠올릴 때 그리는 그림이 PT_QUERY_INFO::q.select다. 여기에는 list(select 리스트), from, where, group_by, having, connect_by, start_with, using_index, 힌트 슬롯들, order_by(UNION도 공유하기 때문에 PT_QUERY_INFO::order_by로 끌어 올림), limit, for_update, 그리고 열 몇 가지가 더 들어 있다. 이 모두는 PT_NODE * 자식이다. 문법이 reduce해 넣은 무엇이든 가리킨다.

노드 타입별 자식 레이아웃 — pt_apply_f, pt_init_f, pt_print_f

섹션 제목: “노드 타입별 자식 레이아웃 — pt_apply_f, pt_init_f, pt_print_f”

다형성 메커니즘은 모두 크기 PT_NODE_NUMBER인 세 개의 함수 포인터 표다.

// parse_tree_cl.c — apply/init/print function tables (excerpt)
typedef PT_NODE *(*PARSER_INIT_NODE_FUNC) (PT_NODE *);
typedef PARSER_VARCHAR *(*PARSER_PRINT_NODE_FUNC) (PARSER_CONTEXT * parser, PT_NODE * p);
typedef PT_NODE *(*PARSER_APPLY_NODE_FUNC) (PARSER_CONTEXT * parser, PT_NODE * p, void *arg);
static PARSER_INIT_NODE_FUNC *pt_init_f = NULL;
static PARSER_PRINT_NODE_FUNC *pt_print_f = NULL;
static PARSER_APPLY_NODE_FUNC *pt_apply_f = NULL;
static PARSER_APPLY_NODE_FUNC pt_apply_func_array[PT_NODE_NUMBER];
static PARSER_INIT_NODE_FUNC pt_init_func_array[PT_NODE_NUMBER];
static PARSER_PRINT_NODE_FUNC pt_print_func_array[PT_NODE_NUMBER];

parser_init_func_vectors(pt_apply_f == NULL일 때 parser_create_parser로부터 한 번 호출)가 이 배열들을 채운다. PT_SELECT용 apply 함수가 정본(canonical) 예다.

// pt_apply_select — parse_tree_cl.c
static PT_NODE *
pt_apply_select (PARSER_CONTEXT * parser, PT_NODE * p, void *arg)
{
PT_APPLY_WALK (parser, p->info.query.with, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.list, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.from, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.where, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.connect_by, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.start_with, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.after_cb_filter, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.group_by, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.having, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.using_index, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.with_increment, arg);
/* ... more hint slots ... */
PT_APPLY_WALK (parser, p->info.query.into_list, arg);
PT_APPLY_WALK (parser, p->info.query.order_by, arg);
PT_APPLY_WALK (parser, p->info.query.orderby_for, arg);
PT_APPLY_WALK (parser, p->info.query.qcache_hint, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.check_where, arg);
PT_APPLY_WALK (parser, p->info.query.limit, arg);
PT_APPLY_WALK (parser, p->info.query.q.select.for_update, arg);
return p;
}

순서가 중요하다. 모든 워커(의미 검사, 타입 추론, 이름 해소, 플랜 생성, pretty print)가 자식들을 이 순서대로 방문한다. 누군가 PT_QUERY_INFO::q.select에 새 자식을 넣고 여기에 등록하지 않으면 모든 기존 워커에 조용히 보이지 않게 된다. 빌드 경고가 누락을 잡아 주지 않는, 파서 안에서도 몇 안 되는 지점 중 하나다.

pt_init_select는 기본 초기화 쪽의 짝이다.

// pt_init_select — parse_tree_cl.c
static PT_NODE *
pt_init_select (PT_NODE * p)
{
p->info.query.q.select.hint = PT_HINT_NONE;
p->info.query.q.select.check_cycles = CONNECT_BY_CYCLES_ERROR;
p->info.query.all_distinct = PT_ALL;
p->info.query.is_subquery = (PT_MISC_TYPE) 0;
p->info.query.hint = PT_HINT_NONE;
p->info.query.scan_op_type = S_SELECT;
return p;
}

pt_init_f[node->node_type]은 새로 할당된 모든 노드를 parser_init_node가 호출한다. 그래서 어떤 reduce 액션도 새 노드를 만지기 전에 이미 기본값이 세팅되어 있다는 점이다.

워커 — parser_walk_tree / pt_walk_private

섹션 제목: “워커 — parser_walk_tree / pt_walk_private”

parser_walk_tree는 범용 prefix/postfix 순회 인터페이스다.

// parser_walk_tree — parse_tree_cl.c
PT_NODE *
parser_walk_tree (PARSER_CONTEXT * parser, PT_NODE * node,
PT_NODE_WALK_FUNCTION pre_function, void *pre_argument,
PT_NODE_WALK_FUNCTION post_function, void *post_argument)
{
if (node == NULL) return NULL;
PT_WALK_ARG walk_argument;
walk_argument.continue_walk = PT_CONTINUE_WALK;
walk_argument.pre_function = pre_function;
walk_argument.pre_argument = pre_argument;
walk_argument.post_function = post_function;
walk_argument.post_argument = post_argument;
PT_APPLY_WALK (parser, node, &walk_argument);
return node;
}

진짜 엔진은 pt_walk_private다. 노드마다 다음을 한다. (a) pre_function을 호출, (b) pt_apply_f[node_type]로 디스패치해 타입별 자식을 방문한 뒤 data_type, or_next, next 순으로 방문, (c) post_function을 호출. 네 개의 continue_walk 모드 (PT_STOP_WALK, PT_CONTINUE_WALK, PT_LEAF_WALK, PT_LIST_WALK)는 pre 함수가 탐색을 가지치기할 수 있게 해 준다.

// pt_walk_private — parse_tree_cl.c (condensed)
static PT_NODE *
pt_walk_private (PARSER_CONTEXT * parser, PT_NODE * node, void *void_arg)
{
PT_WALK_ARG *walk = (PT_WALK_ARG *) void_arg;
PT_NODE_TYPE node_type;
PARSER_APPLY_NODE_FUNC apply;
int save_continue;
if (walk->pre_function)
{
node = (*walk->pre_function) (parser, node, walk->pre_argument,
&(walk->continue_walk));
if (!node) return NULL;
}
if (walk->continue_walk != PT_STOP_WALK)
{
save_continue = walk->continue_walk;
if (save_continue == PT_CONTINUE_WALK || save_continue == PT_LEAF_WALK)
{
node_type = node->node_type;
if (node_type >= PT_LAST_NODE_NUMBER || !(apply = pt_apply_f[node_type]))
return NULL;
(*apply) (parser, node, walk); /* typed children */
PT_APPLY_WALK (parser, node->data_type, walk); /* data_type child */
}
if (save_continue == PT_CONTINUE_WALK || save_continue == PT_LEAF_WALK
|| save_continue == PT_LIST_WALK)
PT_APPLY_WALK (parser, node->or_next, walk);
if (save_continue == PT_CONTINUE_WALK || save_continue == PT_LIST_WALK)
PT_APPLY_WALK (parser, node->next, walk);
if (walk->continue_walk != PT_STOP_WALK)
walk->continue_walk = save_continue;
}
if (walk->post_function)
node = (*walk->post_function) (parser, node, walk->post_argument,
&(walk->continue_walk));
return node;
}

순회 모양 — pre 방문, apply를 통한 타입별 자식, data_type, or_next, next, post 방문 — 은 고정이다. 사용자가 끼우는 pre_functionpost_function만 달라진다는 점이다. 후속 패스 (pt_resolve_names, pt_check_with_info, pt_semantic_type, pt_to_buildlist_proc, pt_print_tree)가 모두 같은 워커에 꽂혀서 타입별 자식 방문을 매번 재구현하지 않아도 되는 이유가 여기 있다. 짝꿍인 parser_walk_leaves입력 노드의 잎에서 출발하는 변종이다. 입력 노드 자신에 대한 pre 방문은 건너뛰고 곧장 타입별 자식으로 간다.

메모리 모델 — 파서 단위 아레나

섹션 제목: “메모리 모델 — 파서 단위 아레나”

세 번째 핵심은 할당자다. 모든 파스 시점 할당은 두 경로 중 하나를 거쳐 파서 단위 풀로 들어간다.

// parse_tree.c — block-pool sketch (one block per pool node)
#define NODES_PER_BLOCK 256
typedef struct parser_node_block PARSER_NODE_BLOCK;
struct parser_node_block
{
PARSER_NODE_BLOCK *next;
int parser_id;
PT_NODE nodes[NODES_PER_BLOCK];
};
static PARSER_NODE_BLOCK *parser_Node_blocks[HASH_NUMBER];
static PARSER_NODE_FREE_LIST *parser_Node_free_lists[HASH_NUMBER];
static PARSER_STRING_BLOCK *parser_String_blocks[HASH_NUMBER];
static int parser_id = 1;

parser_create_parser가 라이프사이클의 진입점이다.

// parser_create_parser — parse_tree.c (condensed)
PARSER_CONTEXT *
parser_create_parser (void)
{
PARSER_CONTEXT *parser;
struct timeval t;
struct drand48_data rand_buf;
parser = (PARSER_CONTEXT *) calloc (sizeof (PARSER_CONTEXT), 1);
if (parser == NULL)
{
er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_OUT_OF_VIRTUAL_MEMORY,
1, sizeof (PARSER_CONTEXT));
return NULL;
}
#if !defined (SERVER_MODE)
parser_init_func_vectors (); /* lazy fill of pt_apply_f / init_f / print_f */
#endif
parser->id = parser_id++;
if (pt_register_parser (parser) == ER_FAILED)
{
free_and_init (parser);
return NULL;
}
parser->execution_values.row_count = -1;
/* ... seed lrand48 / drand48 / etc ... */
parser->query_id = NULL_QUERY_ID;
/* ... 15 single-bit flags ... */
return parser;
}

자명하지 않은 사실 둘. 첫째, parser_init_func_vectors가 여기서 지연 발화된다 — 한 프로세스에서 처음 만들어지는 파서가 세 함수 포인터 표를 채우는 비용을 치르고, 그 뒤로 만들어지는 모든 파서는 그 결과를 그대로 이어받는다는 점이다. 둘째, 파서는 노드 블록을 직접 소유하지 않는다 — 파서가 소유하는 것은 parser_id이고, 노드 블록은 프로세스 전역 해시(parser_Node_blocks[HASH_NUMBER])에서 조회된다는 점이다. 그래서 parser_free_parser는 죽어 가는 파서의 id로 태깅된 블록을 모두 회수하는 해시 워크가 된다.

노드 할당:

// parser_new_node — parse_tree_cl.c
PT_NODE *
parser_new_node (PARSER_CONTEXT * parser, PT_NODE_TYPE node_type)
{
PT_NODE *node;
node = parser_create_node (parser); /* carve from PARSER_NODE_BLOCK */
if (node)
{
parser_init_node (node, node_type); /* memset + pt_init_f[node_type] */
pt_parser_line_col (node); /* stamp from current YYLTYPE */
node->sql_user_text = g_query_string;
node->sql_user_text_len = g_query_string_len;
}
return node;
}

한 호출 안에서 셋이 일어난다. (a) 파서의 노드 블록 풀에서 PT_NODE 크기의 슬롯을 가져온다. (b) 슬롯을 0으로 채우고, node_type 필드를 세팅하고, pt_init_f[node_type]을 돌려 타입별 유니언 가지의 기본값을 박아 넣는다. (c) (line, column, buffer_pos) 삼중쌍을 렉서의 현재 csql_yylloc / g_query_string_pos로부터 찍어 둔다. 미래의 오류 메시지가 바로 이 노드의 소스 위치를 인용할 수 있게 만들기 위해서다.

parser_init_node가 그 후반부다.

// parser_init_node — parse_tree_cl.c
PT_NODE *
parser_init_node (PT_NODE * node, PT_NODE_TYPE node_type)
{
assert (node != NULL);
assert (node_type < PT_LAST_NODE_NUMBER);
int parser_id = node->parser_id;
memset (node, 0x00, sizeof (PT_NODE));
node->buffer_pos = -1;
node->type_enum = PT_TYPE_NONE;
node->parser_id = parser_id;
node->node_type = node_type;
if (pt_init_f[node_type])
node = (pt_init_f[node_type]) (node);
return node;
}

memset을 가로질러 parser_id를 보존하는 이유는, 슬롯의 정체성 (어느 파서에 속했는가?)이 노드의 논리적 상태보다 오래 살기 때문이다. 이 덕분에 parser_reinit_node는 워크-재작성 도중 사용 중인 슬롯을 다시 찍어 넣어도 슬롯의 홈 파서를 잃지 않는다는 점이다.

짝인 parser_allocPT_NODE아닌 모든 것 — 문자열, 배열, 호스트 변수 배열 — 을 위한 것이다.

// parser_alloc — parse_tree.c (excerpt)
void *
parser_alloc (const PARSER_CONTEXT * parser, const int length)
{
/* ... locate or create a PARSER_STRING_BLOCK with `length` bytes free,
* bump the block's high-water mark, return the new region ... */
}

parser_alloc의 풀에 사는 문자열은 노드와 마찬가지로 파서 컨텍스트와 함께 살고 죽는다. 식별자마다 문법이 부르는 pt_makename(yytext)(내부적으로 pt_append_string을 호출한다)도 새지 않는다는 점이다. 파스 도중 할당된 식별자 문자열의 우주 전체가 parser_free_parser → pt_free_string_blocks에서 한 번에 쓸려 나간다.

문법 액션이 매번 부르는 함수 한 줌이 csql_grammar.y 에필로그에 산다.

// parser_make_expression — csql_grammar.y
PT_NODE *
parser_make_expression (PARSER_CONTEXT * parser, PT_OP_TYPE OP,
PT_NODE * arg1, PT_NODE * arg2, PT_NODE * arg3)
{
PT_NODE *expr = parser_new_node (parser, PT_EXPR);
if (expr)
{
expr->info.expr.op = OP;
if (pt_is_operator_logical (expr->info.expr.op))
expr->type_enum = PT_TYPE_LOGICAL;
expr->info.expr.arg1 = arg1;
expr->info.expr.arg2 = arg2;
expr->info.expr.arg3 = arg3;
/* ... INST_NUM / GROUPBY_NUM / ORDERBY_NUM compatibility checks ... */
/* ... mark parser_cannot_cache for SERIAL / SYS_DATE-family ops ... */
}
return expr;
}
static PT_NODE *
parser_make_link (PT_NODE * list, PT_NODE * node)
{
parser_append_node (node, list);
return list;
}

parser_make_expression은 문법의 모든 이항/삼항 연산자 규칙이 reduce되는 종착지다. <expr> + <expr>, <expr> AND <expr>, <expr> BETWEEN <expr> AND <expr> 모두 여기로 모인다. parser_make_link는 재귀 리스트 규칙(select_list : select_list ',' expr)이 별도 리스트 노드를 할당하지 않고 next 포인터로 리스트를 이어가게 만들어 준다. 이 둘에 인자 개수 검사가 붙은 함수 노드용 parser_make_func_with_arg_count와 식별자 문자열 인터닝용 pt_makename을 더하면, 모든 reduce 액션의 80%가 이 네 가지로 설명된다.

원본 자료의 러닝 예제 SELECT * FROM code WHERE s_name > 'G';는 위의 모든 조각을 한 번에 굴려 본다. 거친 트레이스는 다음과 같다.

  1. 렉서가 토큰 SELECT '*' FROM IDENTIFIER('code') WHERE IDENTIFIER('s_name') '>' STRING_LITERAL('G') ';' 을 반환한다. IDENTIFIERSTRING_LITERALpt_makename / pt_append_bytes로 lexeme을 함께 들고 다닌다. 나머지는 토큰 코드만 들고 다닌다.
  2. 바텀업 파서가 reduce를 진행하면서, 각 규칙의 액션이 알맞은 PT_NODE를 할당하고 자식을 결선한다. 할당 순서는 대략 바텀업 이다. 리터럴 'G'(PT_VALUE, type_enum = PT_TYPE_CHAR)가 먼저 reduce되고, 식별자 s_name(PT_NAME)이 다음, 비교 (PT_EXPR, op = PT_GT)가 그다음, 와일드카드 *(PT_VALUE, type_enum = PT_TYPE_STAR), FROM-spec(PT_SPEC, 그 entity_namecode에 대한 PT_NAME을 가리킨다), 그리고 마지막으로 info.query.q.select.list/from/where이 위의 모두를 가리키는 PT_SELECT 순이다.
  3. 최상위 reduce가 그 결과 PT_SELECTparser->node_stack 위로 push한다.
  4. parser_main이 스택을 parser->statements로 스냅샷해 반환한다.

parser_parse_string이 반환하고 나면, 시스템의 나머지가 보는 것은 PT_SELECT 루트뿐이다. 렉서와 문법은 그림에서 빠진다. 이후의 모든 변환은 pt_apply_f / pt_walk_private로 같은 아레나를 돌아다니며 재작성한다는 점이다.

빌드 결선 — CMake를 통한 Flex/Bison

섹션 제목: “빌드 결선 — CMake를 통한 Flex/Bison”

.l.y 모두 빌드 시점에 Flex/Bison을 돌리는 CMake 커스텀 명령의 입력이다.

# CMakeLists.txt (root) — CSQL FLEX/BISON targets (excerpt)
# Replace old bison directives with new ones; regenerate csql_grammar.yy
# whenever csql_grammar.y is modified.
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/csql_grammar.yy
COMMAND ${CMAKE_COMMAND}
-DGRAMMAR_INPUT_FILE=${CMAKE_SOURCE_DIR}/src/parser/csql_grammar.y
-DGRAMMAR_OUTPUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/csql_grammar.yy
-P ${CMAKE_SOURCE_DIR}/cmake/CSQLGrammarTransform.cmake
MAIN_DEPENDENCY ${CMAKE_SOURCE_DIR}/src/parser/csql_grammar.y
)
set(CSQL_GRAMMAR_INPUT ${CMAKE_CURRENT_BINARY_DIR}/csql_grammar.yy)
bison_target(csql_grammar ${CSQL_GRAMMAR_INPUT} ${CSQL_GRAMMAR_OUTPUT}
COMPILE_FLAGS "--no-lines --name-prefix=csql_yy -d -r all")
flex_target(csql_lexer ${CSQL_LEXER_INPUT} ${CSQL_LEXER_OUTPUT}
COMPILE_FLAGS "--noline --never-interactive --prefix=csql_yy")
add_flex_bison_dependency(csql_lexer csql_grammar)
add_custom_target(gen_csql_grammar DEPENDS ${BISON_csql_grammar_OUTPUTS})
add_custom_target(gen_csql_lexer DEPENDS ${FLEX_csql_lexer_OUTPUTS})

눈여겨볼 두 선택. --name-prefix=csql_yy(생성 심볼이 모두 csql_yy*로 시작하므로 src/loaddb/의 자체 Flex/Bison 문법과 충돌하지 않는다는 점이다)와 csql_grammar.yy라는 간접 단계 — csql_grammar.y는 Bison에 도달하기 전에 CSQLGrammarTransform.cmake전처리 된다는 점이다. 원본 .y를 편집하지 않고도 레거시 YACC 지시어를 Bison-3 문법으로 다시 쓰게 해 주는 장치다.

심볼명을 anchor로 삼는다. 라인번호가 아니다. CUBRID 소스는 시간이 지나면 변한다. 본 섹션 끝의 위치 힌트 표는 문서가 마지막 updated: 시점에 관찰된 값이다.

  • Definitions 블록 — YY_INPUT / YY_USER_ACTION / 시작 조건 / 전역 변수(yybuffer_pos, is_dblink_query_string, expecting_pl_lang_spec).
  • 키워드 규칙 — Bison 토큰 코드를 반환하는 [xX]... 패턴 약 600개. lexeme을 보존해야 하는 키워드는 먼저 csql_yylval.cptr = pt_makename(yytext)를 세팅한다.
  • 식별자 / 숫자 / 문자열 리터럴 규칙.
  • 시작 조건 규칙 — <QUOTED_CHAR_STRING>, <DELIMITED_ID_NAME>, <BRACKET_ID_NAME>, <BACKTICK_ID_NAME>, <PLCSQL_TEXT>, <POST_PLCSQL_TEXT>, <PL_LANG_SPEC> 등.
  • User subroutines 블록 — parser_yyinput과 single/multi-line 변종, csql_yyerror, csql_yyerror_explicit, yywrap, begin_token, parser_c_hint / parser_line_hint.
  • Prologue(%{ ... %}) — buffer_pos를 추가한 YYLTYPE 확장, 헬퍼 컨테이너 typedef(container_2..11), parser_make_func_with_arg_count, parser_make_expression, parser_make_link 선언.
  • Bison 선언 — %locations, %glr-parser, %union, %type(약 1100개), %token(약 600개).
  • 문법 규칙 — stmt, stmt_, select_stmt, select_expression, select_expression_opt_with, select_list, from_list, extended_table_specification, single_table_spec, subquery, expression, predicate, function_call, data_type, create_stmt, alter_stmt, insert_stmt, update_stmt, delete_stmt, merge_stmt … 약 1700개의 production.
  • Epilogue — parser_initialize_parser_context, parser_make_func_with_arg_count, parser_make_expression, parser_make_link, parser_make_link_or, parser_main, parser_init_yydebug.

파스 트리 정의 (src/parser/parse_tree.h)

섹션 제목: “파스 트리 정의 (src/parser/parse_tree.h)”
  • enum pt_node_type — 끝을 가리키는 PT_NODE_NUMBER/PT_LAST_NODE_NUMBER sentinel.
  • enum pt_type_enum — 런타임 값 타입 태그(PT_TYPE_INTEGER, PT_TYPE_VARCHAR, …).
  • 타입별 PT_*_INFO struct (PT_QUERY_INFO, PT_QUERY_INFO 안의 PT_SELECT_INFO, PT_SPEC_INFO, PT_NAME_INFO, PT_EXPR_INFO, PT_VALUE_INFO, PT_DATA_TYPE_INFO, …).
  • union pt_statement_info PT_STATEMENT_INFO.
  • struct parser_nodePT_NODE 자체. 27비트 플래그 묶음과 info 유니언을 가진다.
  • struct parser_context — 노드 스택, statement 배열, 파서별 전역(error_msgs, host_variables, …).
  • enum { PT_STOP_WALK, PT_CONTINUE_WALK, PT_LEAF_WALK, PT_LIST_WALK } — 워커 진행 모드.
  • typedef PT_NODE *(*PT_NODE_WALK_FUNCTION) (...)parser_walk_tree에 끼워 넣을 워커 함수 포인터 타입.

트리/아레나 코드 (src/parser/parse_tree.c)

섹션 제목: “트리/아레나 코드 (src/parser/parse_tree.c)”
  • PARSER_NODE_BLOCK, PARSER_NODE_FREE_LIST, PARSER_STRING_BLOCK — 파서 단위 풀 타입.
  • parser_create_node_block — 블록 리스트 확장.
  • parser_create_nodePT_NODE 슬롯 한 칸 bump 할당.
  • pt_free_node_blocks — 한 파서 id의 블록 전체 해제.
  • parser_create_string_block / parser_allocate_string_buffer — 문자열.
  • pt_register_parser / pt_unregister_parser — 프로세스 전역 해시 등록.
  • parser_alloc — 범용 할당 래퍼.
  • pt_append_string / pt_append_varchar / pt_append_bytes — 문자열 누적.
  • parser_create_parser / parser_free_parser — 컨텍스트 라이프사이클.
  • parser_free_node_resources / parser_free_node — 개별 노드 해제(거의 호출되지 않는다. 보통은 아레나가 일괄로 처리한다).

워커, 초기화, 헬퍼 (src/parser/parse_tree_cl.c)

섹션 제목: “워커, 초기화, 헬퍼 (src/parser/parse_tree_cl.c)”
  • pt_apply_f, pt_init_f, pt_print_fPT_NODE_TYPE별 세 함수 포인터 배열.
  • pt_apply_func_array, pt_init_func_array, pt_print_func_array — 백킹 static 배열.
  • pt_init_apply_f, pt_init_init_f, pt_init_print_f — 세 배열을 채우는 함수.
  • parser_init_func_vectorsparser_create_parser에서 호출 되는 지연 진입점.
  • pt_walk_private — 재귀 워커. parser_walk_treeparser_walk_leaves가 공유한다.
  • parser_walk_tree, parser_walk_leaves, pt_continue_walk — 공개 순회 API.
  • parser_new_node, parser_init_node, parser_reinit_node — 노드 생성과 재초기화.
  • parser_free_tree, parser_free_subtrees — 명시적 서브트리 해제(파서가 죽기 전에 서브트리를 다시 작성하는 변환에서 쓰인다).
  • parser_copy_tree, parser_copy_tree_listcopy_node_in_tree_pre를 사용한 깊은 복사.
  • parser_append_node, parser_append_node_or — 리스트 스파인 헬퍼.
  • pt_apply_select, pt_init_select, pt_print_selectPT_SELECT용 정본 예제. 모든 PT_NODE_TYPE이 평행한 세 함수를 갖는다.
  • parser_parse_string, parser_parse_string_with_escapes, parser_parse_string_use_sys_charset, parser_parse_file — 공개 진입점.
  • parser_main, parser_create_parser, parser_free_parser, parser_init_func_vectors, parser_alloc.
  • parser_parse_string, parser_parse_string_with_escapes, parser_parse_string_use_sys_charset, parser_parse_file.
  • parser_new_node, parser_init_node, parser_reinit_node, parser_free_tree, parser_free_subtrees, parser_clear_node, parser_free_node, parser_free_node_resources.
  • parser_walk_tree, parser_walk_leaves, pt_continue_walk.
  • parser_print_tree, parser_print_tree_with_quotes, parser_print_tree_list.

스캐너 지원 (src/parser/scanner_support.c)

섹션 제목: “스캐너 지원 (src/parser/scanner_support.c)”
  • pt_makename — 식별자 문자열을 파서별 아레나에 인터닝하고 char *를 반환.
  • pt_get_keyword / pt_check_word_for_keyword — 후보 식별자가 실제 예약어인지 조회.
  • dbcs_get_next / buffgetin / binarygetin / fgetin — 파서 컨텍스트가 렉서 호출 직전에 결선해 주는 next_char / next_byte 구현.

이 개정 시점의 위치 힌트 (2026-04-30 기준)

섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30 기준)”
심볼파일라인
enum pt_node_type (PT_NODE_TYPE)parse_tree.h963
PT_LAST_NODE_NUMBERparse_tree.h1091
enum pt_type_enum (PT_TYPE_ENUM)parse_tree.h1096
union pt_statement_infoparse_tree.h3470
struct parser_node (the PT_NODE itself)parse_tree.h3649
struct parser_contextparse_tree.h3766
PT_NODE_WALK_FUNCTION typedefparse_tree.h1737
PT_STOP_WALK / CONTINUE / LEAF / LISTparse_tree.h882
parser_maincsql_grammar.y23678
parser_make_expressioncsql_grammar.y22573
parser_make_linkcsql_grammar.y22633
parser_make_func_with_arg_countcsql_grammar.y22542
parser_initialize_parser_contextcsql_grammar.y23480
select_stmt rulecsql_grammar.y12848
select_expression rulecsql_grammar.y12365
select_expression_opt_with rulecsql_grammar.y12263
stmt rulecsql_grammar.y1776
%unioncsql_grammar.y545
%locations / %glr-parsercsql_grammar.y540
pt_walk_privateparse_tree_cl.c914
parser_walk_leavesparse_tree_cl.c1001
parser_walk_treeparse_tree_cl.c1045
parser_new_nodeparse_tree_cl.c2245
parser_init_nodeparse_tree_cl.c2266
parser_reinit_nodeparse_tree_cl.c2292
parser_append_nodeparse_tree_cl.c3238
pt_init_apply_fparse_tree_cl.c5016
pt_init_init_fparse_tree_cl.c5145
pt_init_print_fparse_tree_cl.c5280
pt_apply_selectparse_tree_cl.c14302
pt_init_selectparse_tree_cl.c14340
pt_print_selectparse_tree_cl.c14358
parser_init_func_vectorsparse_tree_cl.c17754
parser_parse_stringparse_tree_cl.c1800
parser_parse_string_with_escapesparse_tree_cl.c1813
parser_parse_string_use_sys_charsetparse_tree_cl.c1782
parser_parse_fileparse_tree_cl.c1924
parser_create_parserparse_tree.c1174
parser_free_parserparse_tree.c1253
parser_create_nodeparse_tree.c276
parser_create_node_blockparse_tree.c219
parser_allocparse_tree.c956
parser_main declarationparser.h68
parser_create_parser declarationparser.h73
parser_walk_tree declarationparser.h114
pt_makenamescanner_support.c213

원본 분석 자료(PDF “Parser - Lexing and Parsing SQL v1.0”, PDF “Parser - Parsing Tree Structure v1.0”, PPTX “parser_semantic_check_basic v1.0”)는 CUBRID의 더 이른 브랜치를 작성되었다. 위의 소스 상태 기준으로 검증된 차이는 다음과 같다.

  • parser_yyinput은 이제 셋이 아니라 하나의 함수다. PDF는 user-subroutines 블록에 parser_yyinput, parser_yyinput_single_line, parser_yyinput_multi_line을 서로 평행한 셋으로 그렸다. 현재 csql_lexer.lparser_yyinput만 활성 reader로 선언한다. 옛 multi-line 헬퍼는 parser_yyinput_single_mode의 일회용 모드에서만 참조될 뿐 기본 경로가 아니다. 일반적인 다중 statement 문자열에서는 동작이 같다.
  • 자료의 extern int yyline; extern int dot_flag; 선언은 csql_lexer.l 위쪽에 더 이상 보이지 않는다. 현재 소스에는 extern int yycolumnextern int yycolumn_end만 있으며, 줄 추적은 Flex 자체의 %option yylineno로 이주했다는 점이다. 자료의 스니펫은 그 전환 이전 시점의 코드다.
  • parser_make_linkvoid가 아니라 PT_NODE *를 반환한다. Lexing and Parsing SQL PDF 하단의 parser_make_link(반환형 PT_NODE *)는 정확하다. 현재 소스와 일치한다.
  • 자료의 Parsing Tree Structure PDF에 있는 pt_walk_private 본문은 한 가지 구조 변경을 빼면 현재 모습 그대로다. PDF의 if (node && walk->pre_function) 가드는 무조건적 pre 호출로 단순화되었다(호출 측이 이미 null 검사를 한다는 점이다). 끝에서 하던 data_type 방문은 이제 타입별 자식 가지 에 접혀 들어와 (*apply) 다음 PT_APPLY_WALK (parser, node->data_type, walk) 로 위치한다. 네 개의 continue_walk 모드(PT_STOP_WALK, PT_CONTINUE_WALK, PT_LEAF_WALK, PT_LIST_WALK)와 그 의미는 변하지 않았다.
  • PT_NODE::flag는 자유 int가 아니라 27비트 struct다. 자료의 PT_NODE 다이어그램은 etc flag를 로 흐려 두었다. 비트필드를 얼버무린 셈이다. 현재 소스는 27개의 명명된 단일 비트 플래그를 갖는다. recompile, cannot_prepare, partition_pruned, si_datetime, si_tran_id, clt_cache_check, use_plan_cache, is_hidden_column, with_rollup, do_not_fold, is_value_query, do_not_replace_orderby, is_added_by_parser, is_alias_enabled_expr, is_wrapped_res_for_coll, is_system_generated_stmt, use_auto_commit, done_reduce_equality_terms, print_in_value_for_dblink, do_not_use_subquery_cache, for_default_func 등이 포함된다. 나머지는 이전부터 있던 것들이다.
  • PT_NODEnext_row 링크는 PT_NODE_LIST / VALUES 질의 행을 위한 것이다. 자료는 이를 다루지 않는다. 현재 소스는 PT_SELECT의 values 형태로 생성된 질의에서 PT_NODE_LIST에 속하는 PT_VALUE / PT_NAME / PT_EXPR 노드에 이를 사용한다. 워커(pt_walk_private)는 next_row를 직접 따라가지 않는다. PT_NODE_LIST의 apply가 타입별 자식 경로로 자기 안의 행 리스트를 명시적으로 방문하는 방식이다.
  • parser_init_func_vectors는 이제 parser_create_parser로부터 지연 발화된다(#if !defined (SERVER_MODE) 아래에서). 자료는 이 함수를 프로세스 시작 시점에 호출되는 것으로 그렸지만, 현재 소스는 한 프로세스 안에서 처음 파서 컨텍스트가 만들어질 때 호출 된다.
  • PT_NODE::sql_user_text는 노드별이며 parser_new_node 안에서 세팅된다. 자료는 이를 언급하지 않는다. 이 필드 덕분에 플랜 캐시는 노드가 속한 statement의 원본 SQL 문자열로 키를 잡을 수 있고, statement 단위 EXPLAIN도 다시 렌더링된 문자열이 아닌 원본 텍스트를 인용할 수 있다는 점이다.
  1. GLR 충돌 개수가 측정되어 있지 않다. 문법은 시프트/리듀스 모호성 때문에 %glr-parser를 선언했다. 그렇다면 실제로 모호한 상태는 몇 개이며, 어떤 구문이 그 책임인가? 추적 경로: bison_targetCOMPILE_FLAGS에 이미 들어 있는 -r all -d로 빌드한 뒤 csql_grammar.output을 살펴 State has conflicts 항목 수를 세는 것. 자료는 이 점을 다루지 않는다.
  2. csql_grammar.yy 전처리기의 범위. cmake/CSQLGrammarTransform.cmake은 Bison이 보기 전에 csql_grammar.ycsql_grammar.yy로 다시 쓴다. 이 재작성은 무엇을 하는가. 폐기된 YACC 지시어(%pure_parser%define api.pure)를 교체하는가? reduce 액션 문법을 다시 쓰는가? 모든 문법 편집이 이 필터를 거쳐 가기 때문에 중요한 질문이다. 추적 경로: CSQLGrammarTransform.cmake을 읽고, 디버그 빌드에서 입출력 한 쌍을 골라 diff하기.
  3. expecting_pl_lang_spec은 일회용 플래그다. 렉서는 문법이 프로시저 본체를 막 소비하려 할 때 PLCSQL_TEXT 시작 조건으로 전환하기 위해 이 플래그를 본다. 프로시저 본체 도중 파스 오류가 나면 이 플래그가 어떻게 리셋되는가. PLCSQL_TEXT 안에서의 오류가 시작 조건 상태를 다음 parser_main 호출로 새지 않게 막아 주는가? 추적 경로: csql_lexer.l 안의 BEGIN(INITIAL) 호출을 추적하고, parser_main의 진입 시점 expecting_pl_lang_spec = 0이 회복에 충분한지 확인 (자명하지 않다. Flex의 시작 조건 스택은 이 변수와 별개다).
  4. parser_main 재진입 시 메모리 회복. 주석 parser_main can be reentered while executing statements loaded by loaddb -s. During the loaddb -s, the yybuffer_pos must not be curruptedyybuffer_pos를 입출구에서 저장/복원한다. 다른 thread-local 유사 파서 전역들(g_query_string, g_query_string_pos, parser_select_level, parser_within_join_condition)도 재진입에서 보호되어야 하는데 현재는 그렇지 않은 것 아닌가? 추적 경로: csql_grammar.y에서 ^static .* parser_을 grep하고, 각각 재진입 안전성을 감사하기.
  5. parser_id 오버플로. parse_tree.c의 static parser_id(static int parser_id = 1;)는 parser_Node_blocks[HASH_NUMBER]의 키다. 2³¹번 파서를 만들고 나면 오버플로한다. 장수하는 cub_server(또는 부하가 큰 broker CAS)에서 이 지점에 도달하는가, 도달하면 어떻게 되는가. 단지 해시 체인이 충돌해 옛 블록이 반환되는가, 아니면 가드가 있는가? 추적 경로: pt_register_parser의 충돌 검사를 읽기.
  6. PT_NODE::flag 대 struct 정렬 증가. 플래그 struct는 현재 27개의 단일 비트 필드를 갖는다. 한 비트를 더 추가하면 32비트 호스트에서 두 워드로 밀려난다. PT_NODE는 256개씩 묶여 할당 되므로, 한 머신 워드만큼만 자라도 풀의 블록당 메모리 발자국이 8 KB 늘어난다. 이를 추적하는 사람이 있는가? 추적 경로: 최근 커밋들에 걸쳐 sizeof (PT_NODE) 스냅샷.

원본 분석 (raw/code-analysis/cubrid/query-processing/)

섹션 제목: “원본 분석 (raw/code-analysis/cubrid/query-processing/)”
  • code_analysis_Parser-Lexing_and_Parsing_SQL_v1_0.pdf — 렉스/ 파스 파이프라인, csql_lexer.lcsql_grammar.y의 파일 단위 워크스루, 빌드 결선 스니펫.
  • code_analysis_Parser-Parsing_Tree_Structure_v_1_0.pdfPT_NODE 구조, (node, info, walk) 삼위일체 프레이밍, 순회의 BST 비유, pt_walk_private 본문.
  • parser_semantic_check_basic_v1.0.pptx — 질의 처리 파이프라인 다이어그램, PT_NODE 빠른 참조, 러닝 예제 SELECT * FROM code WHERE s_name > 'G', 조인 / DOT 경로 / 파생 테이블 / Oracle outer 조인의 전체 예제 트리. 본 문서는 파서 관련 슬라이드를 사용했고, 의미 검사 절반은 다음 코드 분석 문서의 상류에 해당한다.
  • _converted/codeanalysisparser-lexingandparsingsqlv10.pdf.md, _converted/codeanalysisparser-parsingtreestructurev10.pdf.md, _converted/parsersemanticcheckbasicv1.0.pptx.md — 위의 markitdown 추출본.
  • knowledge/code-analysis/cubrid/cubrid-mvcc.md — MVCC 가시성 검사는 의미 검사가 끝난 뒤의 PT_NODE 트리에 대해서만 동작 한다. 파서는 MVCC 코드 경로의 상류에 위치한다.
  • Database Internals(Petrov), 1장 §Query Processor — 파서가 어디에 자리 잡는지에 대한 아키텍처 그림. 모든 후속 패스가 따라 가는 트리의 생산자로서 파서를 프레이밍한 출처.
  • Aho, Lam, Sethi, Ullman, Compilers: Principles, Techniques, and Tools(Dragon book), 2판 — 3장(어휘 분석, 정규 언어 → DFA, Lex), 4.7장(LALR(1)), 4.1.4장(오류 회복), 5장(구문 지시 번역, 의미 액션). Flex/Bison이 내부적으로 무엇을 생성하는지에 대한 표준 참조.
  • Bison 매뉴얼(GNU Free Software Foundation) — %glr-parser 의미론, %locations, csql_yyparse / csql_yylex 드라이버 계약, YYLTYPE / csql_yylval 상호작용.
  • PostgreSQL 소스 — src/backend/parser/scan.l, src/backend/parser/gram.y, src/include/nodes/nodes.h (CUBRID의 PT_NODE / PT_NODE_TYPE이 미러링한 Node/NodeTag 패턴).
  • MySQL 소스 — sql/sql_yacc.yy, sql/sql_lex.cc (CUBRID 패턴 과 비슷한, GLR 풍의 큰 문법).

CUBRID 소스 (/data/hgryoo/references/cubrid/)

섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”
  • src/parser/csql_lexer.l — Flex 소스.
  • src/parser/csql_grammar.y — Bison 소스 + parser_main과 reduce 액션 헬퍼.
  • src/parser/parse_tree.hPT_NODE, PT_NODE_TYPE, PT_STATEMENT_INFO, parser_context, 워커 typedef.
  • src/parser/parse_tree.c — 파서별 아레나, 라이프사이클.
  • src/parser/parse_tree_cl.c — 워커, init 벡터, 타입별 apply/init/print, 공개 parser_parse_* 래퍼, 노드 생성 진입점.
  • src/parser/parser.h — 공개 API 표면.
  • src/parser/scanner_support.cpt_makename, next_char / next_byte 구현.
  • cmake/CSQLGrammarTransform.cmake, CMakeLists.txt(루트) — flex_target / bison_target 결선.