Skip to content

Commit

Permalink
Add stubgen for Pascal (#82)
Browse files Browse the repository at this point in the history
* Add forward-declarations preprocessor

* Add Pascal templates

* Support `case_insensitive_keywords` option in stub_config.toml

---------

Co-authored-by: ellnix <[email protected]>
Co-authored-by: Andriamanitra <[email protected]>
  • Loading branch information
3 people authored Jun 27, 2024
1 parent 33a84d3 commit 88685b5
Show file tree
Hide file tree
Showing 17 changed files with 329 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ ident }} : {{ type_tokens[var_type] }};{% if input_comment %} // {{ input_comment }} {% endif %}
6 changes: 6 additions & 0 deletions config/stub_templates/pascal/loop.pas.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
for {{ index_ident }} := 0 to {{ count_var }} - 1 do
begin
{%- for line in inner %}
{{line}}
{%- endfor %}
end;
24 changes: 24 additions & 0 deletions config/stub_templates/pascal/loopline.pas.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{%- set vars_length = vars | length -%}
{%- if vars_length == 1 -%}
{%- set offset = "" -%}
{%- else -%}
{%- set offset = vars_length ~ "*" -%}
{%- endif -%}

ParseIn(Inputs);
for {{ index_ident }} := 0 to {{ count_var }} - 1 do
begin
{%- for var in vars %}
{%- if loop.index0 == 0 -%}
{%- set idx = "" -%}
{%- else -%}
{%- set idx = "+" ~ loop.index0 -%}
{%- endif -%}
{%- if var.var_type == "Word" -%}
{%- set assign_var = var.ident ~ " := Inputs[" ~ offset ~ index_ident ~ idx ~ "];" -%}
{%- else -%}
{%- set assign_var = var.ident ~ " := " ~ type_parsers[var.var_type] ~ "(Inputs[" ~ offset ~ index_ident ~ idx ~ "]);" -%}
{%- endif %}
{{ assign_var }}
{%- endfor %}
end;
6 changes: 6 additions & 0 deletions config/stub_templates/pascal/main.pas.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% for line in statement %}
// {{ line }}
{%- endfor %}
{% for line in code_lines %}
{{ line }}
{%- endfor %}
25 changes: 25 additions & 0 deletions config/stub_templates/pascal/main_wrapper.pas.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
program Answer;
{$H+}
uses sysutils, classes, math;

// Helper to read a line and split tokens
procedure ParseIn(Inputs: TStrings) ;
var Line : string;
begin
readln(Line);
Inputs.Clear;
Inputs.Delimiter := ' ';
Inputs.DelimitedText := Line;
end;

var
{%- for line in forward_declarations %}
{{ line }}
{%- endfor %}
Inputs : TStringList;
begin
Inputs := TStringList.Create;
{%- for line in main_contents %}
{{ line }}
{%- endfor %}
end.
9 changes: 9 additions & 0 deletions config/stub_templates/pascal/read_many.pas.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ParseIn(Inputs);
{% for var in vars -%}
{%- if var.var_type == "Word" -%}
{%- set assign_var = var.ident ~ " := Inputs[" ~ loop.index0 ~ "];" -%}
{%- else -%}
{%- set assign_var = var.ident ~ " := " ~ type_parsers[var.var_type] ~ "(Inputs[" ~ loop.index0 ~ "]);" -%}
{%- endif -%}
{{ assign_var }}
{% endfor -%}
17 changes: 17 additions & 0 deletions config/stub_templates/pascal/read_one.pas.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{%- if var.var_type == "String" -%}
{%- set readln = "readln(" ~ var.ident ~ ");" -%}
{%- else -%}
{%- set parse_input = "ParseIn(Inputs);" -%}
{%- if var.var_type == "Word" -%}
{%- set assign_var = var.ident ~ " := Inputs[0];" -%}
{%- else -%}
{%- set assign_var = var.ident ~ " := " ~ type_parsers[var.var_type] ~ "(Inputs[0]);" -%}
{%- endif -%}
{%- endif -%}

{%- if var.var_type == "String" -%}
{{ readln }}
{% else -%}
{{ parse_input }}
{{ assign_var }}
{% endif -%}
52 changes: 52 additions & 0 deletions config/stub_templates/pascal/stub_config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name = "pascal"
source_file_ext = "pas"

preprocessor = "forward-declarations"

[type_tokens]
Int = "Int32"
Long = "Int64"
Float = "Extended"
Bool = "Int32"
String = "String"
Word = "String"

[type_parsers]
Int = "StrToInt"
Long = "StrToInt64"
Float = "StrToFloat"
Bool = "StrToInt"

[variable_name_options]
casing = "pascal_case"
allow_uppercase_vars = false
keywords = [
# Special CG variables. Inputs is a buffer-like variable used for parsing,
# and Answer is the name of the program.
"inputs", "answer",

# Others
"boolean",

# https://wiki.freepascal.org/Reserved_words#Reserved_words_in_Turbo_Pascal
"and", "array", "asm", "begin", "break", "case", "const", "constructor",
"continue", "destructor", "div", "do", "downto", "else", "end",
"false", "file", "for", "function", "goto", "if", "implementation",
"in", "inline", "interface", "label", "mod", "nil", "not", "object",
"of", "on", "operator", "or", "packed", "procedure", "program", "record",
"repeat", "set", "shl", "shr", "string", "then", "to", "true", "type",
"unit", "until", "uses", "var", "while", "with", "xor",

# https://wiki.freepascal.org/Reserved_words#Reserved_words_in_Object_Pascal
"as", "class", "constref", "dispose", "except", "exit", "exports",
"finalization", "finally", "inherited", "initialization", "is", "library",
"new", "on", "out", "property", "raise", "self", "threadvar", "try",

# https://wiki.freepascal.org/Reserved_words#Modifiers_(directives)
"absolute", "abstract", "alias", "assembler", "cdecl", "Cppdecl",
"default", "export", "external", "forward", "generic", "index", "local",
"name", "nostackframe", "oldfpccall", "override", "pascal", "private",
"protected", "public", "published", "read", "register", "reintroduce",
"safecall", "softfloat", "specialize", "stdcall", "virtual", "write"
]
case_insensitive_keywords = true
7 changes: 7 additions & 0 deletions config/stub_templates/pascal/write.pas.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{%- for line in output_comments %}
// {{ line }}
{% endfor %}
{%- for line in messages -%}
writeln('{{ line | replace(from="'", to="''") }}');
{% endfor -%}
flush(StdErr); flush(output); // Codingame compliance
1 change: 1 addition & 0 deletions config/stub_templates/pascal/write_join.pas.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// UNSUPPORTED WRITE_JOIN
5 changes: 5 additions & 0 deletions src/stub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,9 @@ mod tests {
fn test_reference_stub_cpp() {
generate("cpp", COMPLEX_REFERENCE_STUB).unwrap();
}

#[test]
fn test_reference_stub_pascal() {
generate("pascal", COMPLEX_REFERENCE_STUB).unwrap();
}
}
12 changes: 12 additions & 0 deletions src/stub/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ pub(super) struct TypeTokens {
pub(super) struct Language {
pub variable_name_options: VariableNameOptions,
pub source_file_ext: String,
// NOTE: These comments are for a future PR
//
// Generic, used for either type keywords...:
// - float in: float a_float; (C)
// - i32 in: let an_int: i32; (Rust)
// ... or parsing functions:
// - int in: x = int(input()) i (Python)
// - StrToInt in: x = StrToInt(Inputs[0]) (Pascal)
pub type_tokens: TypeTokens,
// But sometimes you need two tokens per type for a language.
// - Int32 and StrToInt for Pascal.
pub type_parsers: Option<TypeTokens>,
#[serde(deserialize_with = "deser_preprocessor", default)]
pub preprocessor: Option<Preprocessor>,
}
Expand All @@ -33,6 +44,7 @@ where
let preprocessor: String = Deserialize::deserialize(deserializer)?;
match preprocessor.as_str() {
"lisp-like" => Ok(Some(preprocessor::lisp_like::transform)),
"forward-declarations" => Ok(Some(preprocessor::forward_declarations::transform)),
_ => Err(D::Error::custom(format!("preprocessor {preprocessor} not found."))),
}
}
55 changes: 49 additions & 6 deletions src/stub/language/variable_name_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ enum Casing {
#[serde(rename_all = "snake_case")]
pub struct VariableNameOptions {
casing: Casing,
allow_uppercase_vars: Option<bool>,
#[serde(default)]
allow_uppercase_vars: bool,
keywords: Vec<String>,
/// Set to true if the casing of keywords does not matter in the language
/// (default=false). Used when determining if an identifier needs to be
/// renamed when generating a stub.
#[serde(default)]
case_insensitive_keywords: bool,
}

fn is_uppercase_string(string: &str) -> bool {
Expand All @@ -30,10 +36,10 @@ impl VariableNameOptions {
// CG has special treatment for variables with all uppercase identifiers.
// In most languages they remain uppercase regardless of variable format.
// In others (such as ruby where constants are uppercase) they get downcased.
let converted_variable_name = match (is_uppercase_string(variable_name), self.allow_uppercase_vars) {
(true, Some(false)) => variable_name.to_lowercase(),
(true, _) => variable_name.to_string(),
(false, _) => self.convert(variable_name),
let converted_variable_name = match is_uppercase_string(variable_name) {
true if self.allow_uppercase_vars => variable_name.to_string(),
true => variable_name.to_lowercase(),
false => self.convert(variable_name),
};

self.escape_keywords(converted_variable_name)
Expand All @@ -48,8 +54,19 @@ impl VariableNameOptions {
}
}

/// Escapes a variable name if it is contained in the vector of (disallowed)
/// keywords.
fn escape_keywords(&self, variable_name: String) -> String {
if self.keywords.contains(&variable_name) {
// This is language dependent:
// "string STRING" is valid cpp but "STRING : String" is not valid Pascal
// even though the keyword "string" is expected to be escaped in both languages.
let is_equal = if self.case_insensitive_keywords {
str::eq_ignore_ascii_case
} else {
<str as PartialEq>::eq
};

if self.keywords.iter().any(|kw| is_equal(kw, &variable_name)) {
format!("_{variable_name}")
} else {
variable_name
Expand Down Expand Up @@ -136,4 +153,30 @@ mod tests {
assert_eq!("phrase1BrailleTopRow", convert("Phrase1BrailleTopRow"));
assert_eq!("craneASCIIRepresentation", convert("craneASCIIRepresentation"));
}

#[test]
fn test_keywords_case_sensitive() {
let variable_name_options = VariableNameOptions {
casing: Casing::SnakeCase,
allow_uppercase_vars: true,
keywords: vec!["boolean".to_string()],
case_insensitive_keywords: false,
};

// Does not change Boolean into _Boolean
assert_eq!(variable_name_options.escape_keywords("Boolean".to_string()), "Boolean".to_string());
}

#[test]
fn test_keywords_case_insensitive() {
let variable_name_options = VariableNameOptions {
casing: Casing::SnakeCase,
allow_uppercase_vars: true,
keywords: vec!["boolean".to_string()],
case_insensitive_keywords: true,
};

// Changes Boolean into _Boolean
assert_eq!(variable_name_options.escape_keywords("Boolean".to_string()), "_Boolean".to_string());
}
}
1 change: 1 addition & 0 deletions src/stub/preprocessor.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod forward_declarations;
pub mod lisp_like;

use dyn_clone::DynClone;
Expand Down
108 changes: 108 additions & 0 deletions src/stub/preprocessor/forward_declarations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use super::Renderable;
use crate::stub::renderer::ALPHABET;
use crate::stub::{Cmd, Stub, VarType, VariableCommand};

/// Change the Stub structure into: [ReadDeclarations, MainContents(old_cmds)]
/// This is relevant for Pascal.
#[derive(Debug, Clone)]
struct MainWrapper {
// Read declarations that should go on top of the main function.
// render declaration: int c;
// render read (usual): int c;\nscanf("%d", c);
pub forward_declarations: Vec<VariableCommand>,
// The main function contents.
pub main_content: Vec<Cmd>,
}

/// Edit stub to allow for Pascal-style forward declarations.
///
/// Wraps all of the commands in a stub that contains:
/// - Forward declarations
/// - The rest of the code
///
/// Traverses through the stub commands, taking all declared variables.
/// Leaves only one MainWrapper command in the stub.
/// Introduces two new templates:
/// - `forward_declaration` - similar to a read_one declares one single
/// variable, includes all the fields in a VariableCommand (not nested under
/// `var`)
/// - `main_wrapper` - wraps all of the code, contains `forward_declarations`
/// (the above resource, rendered) and `main_content`
pub fn transform(stub: &mut Stub) {
let mut max_nested_depth = 0;

let mut forward_declarations: Vec<VariableCommand> = stub
.commands
.iter()
.filter_map(|cmd| {
let (cmd, nested_depth) = unpack_cmd(cmd, 0);

if nested_depth > max_nested_depth {
max_nested_depth = nested_depth;
}

match cmd {
Cmd::LoopLine {
variables: var_cmds, ..
} => Some(var_cmds.into_iter()),
Cmd::Read(var_cmds) => Some(var_cmds.into_iter()),
_ => None,
}
})
.flatten()
.collect();

// Add the loop indices to the declarations
forward_declarations.extend(ALPHABET[0..max_nested_depth].iter().map(|loop_var| VariableCommand {
ident: loop_var.to_string(),
var_type: VarType::Int,
max_length: None,
input_comment: String::new(),
}));

let mut seen = std::collections::BTreeSet::<String>::new();
forward_declarations.retain(|var_cmd| seen.insert(var_cmd.ident.clone()));

let wrapper = MainWrapper {
forward_declarations,
main_content: stub.commands.drain(..).collect(),
};

stub.commands = vec![Cmd::External(Box::new(wrapper))];
}

fn unpack_cmd(cmd: &Cmd, nested_depth: usize) -> (Cmd, usize) {
match cmd {
Cmd::Loop {
count_var: _,
command: subcmd,
} => unpack_cmd(subcmd, nested_depth + 1),
Cmd::LoopLine { .. } => (cmd.clone(), nested_depth + 1),
_ => (cmd.clone(), nested_depth),
}
}

impl Renderable for MainWrapper {
fn render(&self, renderer: &crate::stub::renderer::Renderer) -> String {
let main_contents_str: String =
self.main_content.iter().map(|cmd| renderer.render_command(cmd, 0)).collect();
let main_contents: Vec<&str> = main_contents_str.lines().collect();

let forward_declarations: Vec<String> =
self.forward_declarations.iter().map(|vc| vc.render(renderer)).collect();

let mut context = tera::Context::new();
context.insert("forward_declarations", &forward_declarations);
context.insert("main_contents", &main_contents);
renderer.tera_render("main_wrapper", &mut context)
}
}

impl Renderable for VariableCommand {
fn render(&self, renderer: &crate::stub::renderer::Renderer) -> String {
let mut context =
tera::Context::from_serialize(self).expect("VariableCommand should be serializable");
context.insert("ident", &renderer.lang.variable_name_options.transform_variable_name(&self.ident));
renderer.tera_render("forward_declarations", &mut context).trim().to_string()
}
}
Loading

0 comments on commit 88685b5

Please sign in to comment.