sources/cg_common.c (329 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#if defined(CQL_AMALGAM_LEAN) && !defined(CQL_AMALGAM_CG_COMMON)
// minimal stubs to avoid link errors
cql_noexport void cg_common_cleanup() {}
void cql_exit_on_semantic_errors(ast_node *head) {}
#else
#include "cg_common.h"
#include "ast.h"
#include "sem.h"
#include "symtab.h"
// Storage declarations
cql_data_defn( symtab *_Nullable cg_stmts );
cql_data_defn( symtab *_Nullable cg_funcs );
cql_data_defn( symtab *_Nullable cg_exprs );
cql_data_defn( charbuf *_Nullable cg_header_output );
cql_data_defn( charbuf *_Nullable cg_main_output );
cql_data_defn( charbuf *_Nullable cg_fwd_ref_output );
cql_data_defn( charbuf *_Nullable cg_constants_output );
cql_data_defn( charbuf *_Nullable cg_declarations_output );
cql_data_defn( charbuf *_Nullable cg_scratch_vars_output );
cql_data_defn( charbuf *_Nullable cg_cleanup_output );
cql_data_defn( charbuf *_Nullable cg_pieces_output );
// Prints a symbol name, along with any configured prefix, to the specified buffer.
// Multiple CSTRs may be supplied to build the name, which will be concatenated
// together. The configured symbol case will be applied to the full symbol name.
// The prefix will be included as specified.
//
// All input names are assumed to be in snake case already.
cql_noexport void cg_sym_name(cg_symbol_case symbol_case, charbuf *_Nonnull output, CSTR _Nonnull symbol_prefix, CSTR _Nonnull name, ...)
{
// Print the prefix first
bprintf(output, symbol_prefix);
// Setup the arg list
va_list args;
va_start(args, name);
CSTR name_component = name;
// Check the case configuration
switch (symbol_case) {
case cg_symbol_case_snake:{
// No need to modify it, everything in here is already snake case.
do {
bprintf(output, "%s", name_component);
} while ((name_component = va_arg(args, CSTR)));
break;
}
case cg_symbol_case_camel:
case cg_symbol_case_pascal:{
// Remove all underscores and uppercase each next character, along with the first if pascal case.
bool should_upper = (symbol_case != cg_symbol_case_camel);
do {
const size_t len = strlen(name_component);
for (size_t i = 0; i != len; ++i) {
if (name_component[i] == '_') {
should_upper = true;
} else if (should_upper) {
bputc(output, Toupper(name_component[i]));
should_upper = false;
} else {
bputc(output, name_component[i]);
}
}
} while ((name_component = va_arg(args, CSTR)));
break;
}
}
va_end(args);
}
#define ALLOC_AND_OPEN_CHARBUF_REF(x) \
(x) = (charbuf *)calloc(1, sizeof(charbuf)); \
bopen(x);
#define CLEANUP_CHARBUF_REF(x) if (x) { bclose(x); free(x); x = NULL; }
cql_noexport void cg_common_init(void)
{
// All of these will leak, but we don't care. The tool will shut down after running cg, so it is pointless to clean
// up after ourselves here.
cg_stmts = symtab_new();
cg_funcs = symtab_new();
cg_exprs = symtab_new();
ALLOC_AND_OPEN_CHARBUF_REF(cg_header_output);
ALLOC_AND_OPEN_CHARBUF_REF(cg_main_output);
ALLOC_AND_OPEN_CHARBUF_REF(cg_fwd_ref_output);
ALLOC_AND_OPEN_CHARBUF_REF(cg_constants_output);
ALLOC_AND_OPEN_CHARBUF_REF(cg_declarations_output);
ALLOC_AND_OPEN_CHARBUF_REF(cg_scratch_vars_output);
ALLOC_AND_OPEN_CHARBUF_REF(cg_cleanup_output);
ALLOC_AND_OPEN_CHARBUF_REF(cg_pieces_output)
}
// lots of AST nodes require no action -- this guy is very good at that.
cql_noexport void cg_no_op(ast_node * ast) {
}
cql_noexport void extract_base_path_without_extension(charbuf *_Nonnull output,
CSTR _Nonnull file_name) {
size_t file_name_length = strlen(file_name);
Contract(file_name_length > 0);
char* last_slash_ptr = strrchr(file_name, '/');
// Slash cannot be the last character of the string
if (last_slash_ptr == &file_name[file_name_length - 1] ) {
cql_error("badly formed file name (trailing '/') '%s'\n", file_name);
cql_cleanup_and_exit(1);
}
size_t begin_index = last_slash_ptr ? (size_t)(last_slash_ptr - file_name) + 1 : 0;
char* last_dot_ptr = strrchr(file_name, '.');
// Dot cannot be the last character of the string
if (last_dot_ptr == &file_name[file_name_length - 1] ) {
cql_error("badly formed file name (trailing '.') '%s'\n", file_name);
cql_cleanup_and_exit(1);
}
size_t end_index = file_name_length;
if (last_dot_ptr) {
size_t last_dot_index = (size_t)(last_dot_ptr - file_name);
if (last_dot_index >= begin_index) {
end_index = last_dot_index;
}
}
if (end_index == begin_index) {
cql_error("badly formed file name (empty base name) '%s'\n", file_name);
cql_cleanup_and_exit(1);
}
for (size_t i = begin_index; i < end_index; i++) {
bputc(output, file_name[i]);
}
}
cql_noexport void cql_exit_on_semantic_errors(ast_node *head) {
if (head && is_error(head)) {
cql_error("semantic errors present; no code gen.\n");
cql_cleanup_and_exit(1);
}
}
cql_noexport void exit_on_no_global_proc() {
if (!global_proc_name) {
cql_error("There are global statements but no global proc name was specified (use --global_proc)\n");
cql_cleanup_and_exit(1);
}
}
// This callback causes select * to be expanded into the appropriate literal
// column names. We don't do this expansion for exists or not exists because
// the columns don't matter.
//
// The tree shape is:
//
// | | | {exists_expr}: bool notnull
// | | | {select_stmt}: select: { id: integer notnull }
// | | | {select_core}: select: { id: integer notnull }
// | | | | {select_expr_list_con}: select: { id: integer notnull }
// | | | | {select_expr_list}: select: { id: integer notnull }
// --
// or
// --
// {sem_select_stmt}: union: { A: integer }
// | {select_core_list}: union: { A: integer }
// | | {select_core}: select: { A: integer variable }
// | | | {select_expr_list_con}: select: { A: integer variable }
// | | | {select_expr_list}: select: { A: integer variable }
// | | | | ...
// | | {select_core_compound}
// | | {int 2}
// | | {select_core_list}: union: { A: integer }
// | | {select_core}: select: { A: integer variable }
// | | | {select_expr_list_con}: select: { A: integer variable }
// | | | {select_expr_list}: select: { A: integer variable }
// | | | | ...
// | | {select_core_compound}
// | | {int 2}
// | | {select_core}: select: { A: integer variable }
// | | {select_expr_list_con}: select: { A: integer variable }
// | | {select_expr_list}: select: { A: integer variable }
// | | | ...
bool_t cg_expand_star(ast_node *_Nonnull ast, void *_Nullable context, charbuf *_Nonnull buffer) {
Contract(is_ast_star(ast) || is_ast_table_star(ast));
Contract(ast->sem);
Contract(!is_error(ast)); // already "ok" at least
Contract(is_struct(ast->sem->sem_type));
if (is_ast_star(ast)) {
EXTRACT(select_expr_list, ast->parent);
EXTRACT(select_expr_list_con, select_expr_list->parent);
EXTRACT(select_core, select_expr_list_con->parent);
EXTRACT_ANY(any_select_core, select_core->parent);
while (!is_ast_select_stmt(any_select_core)) {
any_select_core = any_select_core->parent;
}
EXTRACT_ANY_NOTNULL(select_context, any_select_core->parent);
if (is_ast_exists_expr(select_context)) {
return false;
}
sem_struct *sptr = ast->sem->sptr;
uint32_t count = sptr->count;
bool_t first = true;
for (uint32_t i = 0; i < count; i++) {
if (!first) {
bprintf(buffer, ", ");
}
if (!(sptr->semtypes[i] & SEM_TYPE_HIDDEN_COL)) {
first = false;
bprintf(buffer, "%s", sptr->names[i]);
}
}
}
else {
Invariant(is_ast_table_star(ast));
sem_struct *sptr = ast->sem->sptr;
uint32_t count = sptr->count;
bool_t first = true;
for (uint32_t i = 0; i < count; i++) {
if (!first) {
bprintf(buffer, ", ");
}
if (!(sptr->semtypes[i] & SEM_TYPE_HIDDEN_COL)) {
first = false;
bprintf(buffer, "%s.%s", ast->sem->name, sptr->names[i]);
}
}
}
return true;
}
// Recursively finds table nodes, executing the callback for each that is found. The
// callback will not be executed more than once for the same table name.
cql_noexport void continue_find_table_node(table_callbacks *callbacks, ast_node *node) {
// Check the type of node so that we can find the direct references to tables. We
// can't know the difference between a table or view in the ast, so we will need to
// later find the definition to see if it points to a create_table_stmt to distinguish
// from views.
find_ast_str_node_callback alt_callback = NULL;
symtab *alt_visited = NULL;
ast_node *table_or_view_name_ast = NULL;
if (is_ast_cte_table(node)) {
EXTRACT_ANY_NOTNULL(cte_body, node->right);
// this is a proxy node, it doesn't contribute anything
// any nested select does not run.
if (is_ast_like(cte_body)) {
return;
}
}
else if (is_ast_shared_cte(node)) {
// if we're on a shared CTE usage, then we recurse into the CALL and
// we recurse into the binding list. The CALL should not be handled
// like a normal procedure call, the body is inlined. Note that the
// existence of the fragment is meant to be transparent to anyone
// downstream -- this isn't a normal call that might be invisible to us
// we *must* have the fragment because we're talking about a semantically
// valid shared cte binding.
EXTRACT_NOTNULL(call_stmt, node->left);
EXTRACT(cte_binding_list, node->right);
EXTRACT_ANY_NOTNULL(name_ast, call_stmt->left);
EXTRACT_STRING(name, name_ast);
ast_node *proc = find_proc(name);
if (proc) {
// Look through the proc definition for tables. Just call through recursively.
continue_find_table_node(callbacks, proc);
}
if (cte_binding_list) {
continue_find_table_node(callbacks, cte_binding_list);
}
// no further recursion is needed
return;
}
else if (is_ast_declare_cursor_like_select(node)) {
// There is a select in this declaration but it doesn't really run, it's just type info
// so that doesn't count. So we don't recurse here.
return;
}
else if (is_ast_cte_binding(node)) {
EXTRACT_ANY_NOTNULL(actual, node->left);
// handle this just like a normal table usage in a select statement (because it is)
table_or_view_name_ast = actual;
alt_callback = callbacks->callback_from;
alt_visited = callbacks->visited_from;
}
else if (is_ast_table_or_subquery(node)) {
EXTRACT_ANY_NOTNULL(factor, node->left);
if (is_ast_str(factor)) {
// the other table factor cases (there are several) do not have a string payload
table_or_view_name_ast = factor;
alt_callback = callbacks->callback_from;
alt_visited = callbacks->visited_from;
}
}
else if (is_ast_fk_target(node)) {
// if we're walking a table then we'll also walk its FK's
// normally we don't start by walking tables anyway so this doesn't
// run if you do a standard walk of a procedure
if (callbacks->notify_fk) {
EXTRACT_ANY_NOTNULL(name_ast, node->left);
table_or_view_name_ast = name_ast;
}
}
else if (is_ast_drop_view_stmt(node) || is_ast_drop_table_stmt(node)) {
if (callbacks->notify_table_or_view_drops) {
EXTRACT_ANY_NOTNULL(name_ast, node->right);
table_or_view_name_ast = name_ast;
}
}
else if (is_ast_trigger_target_action(node)) {
if (callbacks->notify_triggers) {
EXTRACT_ANY_NOTNULL(name_ast, node->left);
table_or_view_name_ast = name_ast;
}
}
else if (is_ast_delete_stmt(node)) {
EXTRACT_ANY_NOTNULL(name_ast, node->left);
table_or_view_name_ast = name_ast;
alt_callback = callbacks->callback_deletes;
alt_visited = callbacks->visited_delete;
}
else if (is_ast_insert_stmt(node)) {
EXTRACT(name_columns_values, node->right);
EXTRACT_ANY_NOTNULL(name_ast, name_columns_values->left);
table_or_view_name_ast = name_ast;
alt_callback = callbacks->callback_inserts;
alt_visited = callbacks->visited_insert;
}
else if (is_ast_update_stmt(node)) {
EXTRACT_ANY(name_ast, node->left);
// name_ast node is NULL if update statement is part of an upsert statement
if (name_ast) {
table_or_view_name_ast = name_ast;
alt_callback = callbacks->callback_updates;
alt_visited = callbacks->visited_update;
}
}
else if (is_ast_call_stmt(node) | is_ast_call(node)) {
// Both cases have the name in the node left so we can consolidate
// the check to see if it's a proc is redundant in the call_stmt case
// but it lets us share code so we just go with it. The other case
// is a possible proc_as_func call so we must check if the target is a proc.
EXTRACT_ANY_NOTNULL(name_ast, node->left);
EXTRACT_STRING(name, name_ast);
ast_node *proc = find_proc(name);
if (proc) {
// this only happens for ast_call but this check is safe for both
if (name_ast->sem && (name_ast->sem->sem_type & SEM_TYPE_INLINE_CALL)) {
// Look through the proc definition for tables because the target will be inlined
continue_find_table_node(callbacks, proc);
}
EXTRACT_STRING(canon_name, get_proc_name(proc));
if (callbacks->callback_proc) {
if (symtab_add(callbacks->visited_proc, canon_name, name_ast)) {
callbacks->callback_proc(canon_name, name_ast, callbacks->callback_context);
}
}
}
}
if (table_or_view_name_ast) {
// Find the definition and see if we have a create_table_stmt.
EXTRACT_STRING(table_or_view_name, table_or_view_name_ast);
ast_node *table_or_view = find_table_or_view_even_deleted(table_or_view_name);
// It's not actually possible to use a deleted table or view in a procedure.
// If the name lookup here says that we found something deleted it means
// that we have actually found a CTE that is an alias for a deleted table
// or view. In that case, we don't want to add the thing we found to the dependency
// set we are creating. We don't want to make this CTE an error because
// its reasonable to replace a deleted table/view with CTE of the same name.
// Hence we simply filter out deleted tables/views here.
if (table_or_view && table_or_view->sem->delete_version > 0) {
table_or_view = NULL;
}
// Make sure we don't process a table or view that we've already processed.
if (table_or_view) {
if (is_ast_create_table_stmt(table_or_view)) {
EXTRACT_NOTNULL(create_table_name_flags, table_or_view->left);
EXTRACT_STRING(canonical_name, create_table_name_flags->right);
// Found a table, execute the callback.
if (symtab_add(callbacks->visited_any_table, canonical_name, table_or_view)) {
callbacks->callback_any_table(canonical_name, table_or_view, callbacks->callback_context);
}
// Emit the second callback if any.
if (alt_callback && symtab_add(alt_visited, canonical_name, table_or_view)) {
alt_callback(canonical_name, table_or_view, callbacks->callback_context);
}
} else {
Contract(is_ast_create_view_stmt(table_or_view));
EXTRACT_NOTNULL(view_and_attrs, table_or_view->right);
EXTRACT_NOTNULL(name_and_select, view_and_attrs->left);
EXTRACT_STRING(canonical_name, name_and_select->left);
if (symtab_add(callbacks->visited_any_table, canonical_name, table_or_view)) {
// Report the view itself
if (callbacks->callback_any_view) {
callbacks->callback_any_view(canonical_name, table_or_view, callbacks->callback_context);
}
// Look through the view definition for tables. Just call through recursively.
continue_find_table_node(callbacks, table_or_view);
}
}
}
}
// Check the left and right nodes.
if (ast_has_left(node)) {
continue_find_table_node(callbacks, node->left);
}
if (ast_has_right(node)) {
continue_find_table_node(callbacks, node->right);
}
}
// Find references in a proc and invoke the corresponding callback on them
// this is useful for dependency analysis.
cql_noexport void find_table_refs(table_callbacks *callbacks, ast_node *node) {
// Each kind of callback needs its own symbol table because, for instance,
// you might see a table as an insert and also as an update. If we use
// a single visited table like we used to then the second kind of usage would
// not get recorded.
// Note: we don't need a seperate table for visiting views and visiting tables
// any given name can only be a view or a table, never both.
callbacks->visited_any_table = symtab_new();
callbacks->visited_insert = symtab_new();
callbacks->visited_update = symtab_new();
callbacks->visited_delete = symtab_new();
callbacks->visited_from = symtab_new();
callbacks->visited_proc = symtab_new();
continue_find_table_node(callbacks, node);
SYMTAB_CLEANUP(callbacks->visited_any_table);
SYMTAB_CLEANUP(callbacks->visited_insert);
SYMTAB_CLEANUP(callbacks->visited_update);
SYMTAB_CLEANUP(callbacks->visited_delete);
SYMTAB_CLEANUP(callbacks->visited_from);
SYMTAB_CLEANUP(callbacks->visited_proc);
}
// Produce a crc of a given charbuf using the CRC helpers.
cql_noexport crc_t crc_charbuf(charbuf *input) {
crc_t crc = crc_init();
crc = crc_update(crc, (const unsigned char *)input->ptr, input->used);
return crc_finalize(crc);
}
cql_noexport void cg_common_cleanup() {
SYMTAB_CLEANUP(cg_stmts);
SYMTAB_CLEANUP(cg_funcs);
SYMTAB_CLEANUP(cg_exprs);
CLEANUP_CHARBUF_REF(cg_header_output);
CLEANUP_CHARBUF_REF(cg_main_output);
CLEANUP_CHARBUF_REF(cg_fwd_ref_output);
CLEANUP_CHARBUF_REF(cg_constants_output);
CLEANUP_CHARBUF_REF(cg_declarations_output);
CLEANUP_CHARBUF_REF(cg_scratch_vars_output);
CLEANUP_CHARBUF_REF(cg_cleanup_output);
CLEANUP_CHARBUF_REF(cg_pieces_output)
}
#endif