use crate::types::{Effect, StackType, Type};
use super::{Program, SourceLocation, Statement, WordDef};
const BUILTINS: &[&str] = &[
"io.write",
"io.write-line",
"io.read-line",
"io.read-n",
"int->string",
"symbol->string",
"string->symbol",
"args.count",
"args.at",
"file.slurp",
"file.exists?",
"file.for-each-line",
"file.spit",
"file.append",
"file.delete",
"file.size",
"dir.exists?",
"dir.make",
"dir.delete",
"dir.list",
"string.concat",
"string.length",
"string.byte-length",
"string.char-at",
"string.substring",
"char->string",
"string.find",
"string.split",
"string.contains",
"string.starts-with",
"string.empty?",
"string.trim",
"string.chomp",
"string.to-upper",
"string.to-lower",
"string.equal?",
"string.join",
"string.json-escape",
"string->int",
"symbol.=",
"encoding.base64-encode",
"encoding.base64-decode",
"encoding.base64url-encode",
"encoding.base64url-decode",
"encoding.hex-encode",
"encoding.hex-decode",
"crypto.sha256",
"crypto.hmac-sha256",
"crypto.constant-time-eq",
"crypto.random-bytes",
"crypto.random-int",
"crypto.uuid4",
"crypto.aes-gcm-encrypt",
"crypto.aes-gcm-decrypt",
"crypto.pbkdf2-sha256",
"crypto.ed25519-keypair",
"crypto.ed25519-sign",
"crypto.ed25519-verify",
"net.http.get",
"net.http.post",
"net.http.put",
"net.http.delete",
"list.make",
"list.push",
"list.get",
"list.set",
"list.map",
"list.filter",
"list.fold",
"list.each",
"list.length",
"list.empty?",
"list.reverse",
"list.first",
"list.last",
"map.make",
"map.get",
"map.set",
"map.has?",
"map.remove",
"map.keys",
"map.values",
"map.size",
"map.empty?",
"map.each",
"map.fold",
"variant.field-count",
"variant.tag",
"variant.field-at",
"variant.append",
"variant.first",
"variant.last",
"variant.init",
"variant.make-0",
"variant.make-1",
"variant.make-2",
"variant.make-3",
"variant.make-4",
"wrap-0",
"wrap-1",
"wrap-2",
"wrap-3",
"wrap-4",
"i.add",
"i.subtract",
"i.multiply",
"i.divide",
"i.modulo",
"i.pow",
"i.+",
"i.-",
"i.*",
"i./",
"i.%",
"i.=",
"i.<",
"i.>",
"i.<=",
"i.>=",
"i.<>",
"i.eq",
"i.lt",
"i.gt",
"i.lte",
"i.gte",
"i.neq",
"dup",
"drop",
"swap",
"over",
"rot",
"nip",
"tuck",
"2dup",
"3drop",
"pick",
"roll",
">aux",
"aux>",
"and",
"or",
"not",
"band",
"bor",
"bxor",
"bnot",
"i.neg",
"negate",
"+",
"-",
"*",
"/",
"%",
"=",
"<",
">",
"<=",
">=",
"<>",
"shl",
"shr",
"popcount",
"clz",
"ctz",
"int-bits",
"chan.make",
"chan.send",
"chan.receive",
"chan.close",
"chan.yield",
"call",
"dip",
"keep",
"bi",
"if",
"strand.spawn",
"strand.weave",
"strand.resume",
"strand.weave-cancel",
"yield",
"cond",
"net.tcp.listen",
"net.tcp.connect",
"net.tcp.accept",
"net.tcp.local-port",
"net.tcp.read",
"net.tcp.write",
"net.tcp.close",
"fd->socket",
"socket->fd",
"net.udp.bind",
"net.udp.send-to",
"net.udp.receive-from",
"net.udp.close",
"net.dns.resolve",
"net.tls.client",
"os.getenv",
"os.home-dir",
"os.current-dir",
"os.path-exists",
"os.path-is-file",
"os.path-is-dir",
"os.path-join",
"os.path-parent",
"os.path-filename",
"os.exit",
"os.name",
"os.arch",
"signal.trap",
"signal.received?",
"signal.pending?",
"signal.default",
"signal.ignore",
"signal.clear",
"signal.SIGINT",
"signal.SIGTERM",
"signal.SIGHUP",
"signal.SIGPIPE",
"signal.SIGUSR1",
"signal.SIGUSR2",
"signal.SIGCHLD",
"signal.SIGALRM",
"signal.SIGCONT",
"terminal.raw-mode",
"terminal.read-char",
"terminal.read-char?",
"terminal.width",
"terminal.height",
"terminal.flush",
"f.add",
"f.subtract",
"f.multiply",
"f.divide",
"f.+",
"f.-",
"f.*",
"f./",
"f.=",
"f.<",
"f.>",
"f.<=",
"f.>=",
"f.<>",
"f.eq",
"f.lt",
"f.gt",
"f.lte",
"f.gte",
"f.neq",
"f.sqrt",
"f.cbrt",
"f.pow",
"f.exp",
"f.ln",
"f.log10",
"f.log2",
"f.sin",
"f.cos",
"f.tan",
"f.asin",
"f.acos",
"f.atan",
"f.atan2",
"f.floor",
"f.ceil",
"f.round",
"f.trunc",
"f.pi",
"f.e",
"f.tau",
"int->float",
"float->int",
"float->string",
"string->float",
"int.to-bytes-i32-be",
"float.to-bytes-f32-be",
"test.init",
"test.set-name",
"test.finish",
"test.has-failures",
"test.assert",
"test.assert-not",
"test.assert-eq",
"test.assert-eq-str",
"test.fail",
"test.pass-count",
"test.fail-count",
"time.now",
"time.nanos",
"time.sleep-ms",
"son.dump",
"son.dump-pretty",
"stack.dump",
"regex.match?",
"regex.find",
"regex.find-all",
"regex.replace",
"regex.replace-all",
"regex.captures",
"regex.split",
"regex.valid?",
"compress.gzip",
"compress.gzip-level",
"compress.gunzip",
"compress.zstd",
"compress.zstd-level",
"compress.unzstd",
];
impl Program {
pub fn new() -> Self {
Program {
includes: Vec::new(),
unions: Vec::new(),
words: Vec::new(),
}
}
pub fn find_word(&self, name: &str) -> Option<&WordDef> {
self.words.iter().find(|w| w.name == name)
}
pub fn validate_word_calls(&self) -> Result<(), String> {
self.validate_word_calls_with_externals(&[])
}
pub fn validate_word_calls_with_externals(
&self,
external_words: &[&str],
) -> Result<(), String> {
for word in &self.words {
self.validate_statements(&word.body, &word.name, BUILTINS, external_words)?;
}
Ok(())
}
fn validate_statements(
&self,
statements: &[Statement],
word_name: &str,
builtins: &[&str],
external_words: &[&str],
) -> Result<(), String> {
for statement in statements {
match statement {
Statement::WordCall { name, .. } => {
if builtins.contains(&name.as_str()) {
continue;
}
if self.find_word(name).is_some() {
continue;
}
if external_words.contains(&name.as_str()) {
continue;
}
if let Some(replacement) = v7_renamed_to(name) {
return Err(format!(
"'{}' was renamed to '{}' in v7.0 (called in word '{}'). \
See docs/MIGRATION_7_0.md.",
name, replacement, word_name
));
}
return Err(format!(
"Undefined word '{}' called in word '{}'. \
Did you forget to define it or misspell a built-in?",
name, word_name
));
}
Statement::If {
then_branch,
else_branch,
span: _,
} => {
self.validate_statements(then_branch, word_name, builtins, external_words)?;
if let Some(eb) = else_branch {
self.validate_statements(eb, word_name, builtins, external_words)?;
}
}
Statement::Quotation { body, .. } => {
self.validate_statements(body, word_name, builtins, external_words)?;
}
Statement::Match { arms, span: _ } => {
for arm in arms {
self.validate_statements(&arm.body, word_name, builtins, external_words)?;
}
}
_ => {} }
}
Ok(())
}
const MAX_VARIANT_FIELDS: usize = 12;
pub fn generate_constructors(&mut self) -> Result<(), String> {
let mut new_words = Vec::new();
for union_def in &self.unions {
for variant in &union_def.variants {
let field_count = variant.fields.len();
if field_count > Self::MAX_VARIANT_FIELDS {
return Err(format!(
"Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
Consider grouping fields into nested union types.",
variant.name,
union_def.name,
field_count,
Self::MAX_VARIANT_FIELDS
));
}
let union_ty = Type::Union(union_def.name.clone());
let source = variant.source.clone();
let field_types: Vec<Type> = variant
.fields
.iter()
.map(|f| parse_type_name(&f.type_name))
.collect();
new_words.push(make_helper_word(
format!("Make-{}", variant.name),
&field_types,
union_ty.clone(),
vec![
Statement::Symbol(variant.name.clone()),
Statement::WordCall {
name: format!("variant.make-{}", field_count),
span: None,
},
],
source.clone(),
));
new_words.push(make_helper_word(
format!("is-{}?", variant.name),
std::slice::from_ref(&union_ty),
Type::Bool,
vec![
Statement::WordCall {
name: "variant.tag".to_string(),
span: None,
},
Statement::Symbol(variant.name.clone()),
Statement::WordCall {
name: "symbol.=".to_string(),
span: None,
},
],
source.clone(),
));
for (index, field) in variant.fields.iter().enumerate() {
new_words.push(make_helper_word(
format!("{}-{}", variant.name, field.name),
std::slice::from_ref(&union_ty),
field_types[index].clone(),
vec![
Statement::IntLiteral(index as i64),
Statement::WordCall {
name: "variant.field-at".to_string(),
span: None,
},
],
source.clone(),
));
}
}
}
self.words.extend(new_words);
Ok(())
}
pub fn fixup_union_types(&mut self) {
let union_names: std::collections::HashSet<String> =
self.unions.iter().map(|u| u.name.clone()).collect();
for word in &mut self.words {
if let Some(ref mut effect) = word.effect {
Self::fixup_stack_type(&mut effect.inputs, &union_names);
Self::fixup_stack_type(&mut effect.outputs, &union_names);
}
}
}
fn fixup_stack_type(stack: &mut StackType, union_names: &std::collections::HashSet<String>) {
match stack {
StackType::Empty | StackType::RowVar(_) => {}
StackType::Cons { rest, top } => {
Self::fixup_type(top, union_names);
Self::fixup_stack_type(rest, union_names);
}
}
}
fn fixup_type(ty: &mut Type, union_names: &std::collections::HashSet<String>) {
match ty {
Type::Var(name) if union_names.contains(name) => {
*ty = Type::Union(name.clone());
}
Type::Quotation(effect) => {
Self::fixup_stack_type(&mut effect.inputs, union_names);
Self::fixup_stack_type(&mut effect.outputs, union_names);
}
Type::Closure { effect, captures } => {
Self::fixup_stack_type(&mut effect.inputs, union_names);
Self::fixup_stack_type(&mut effect.outputs, union_names);
for cap in captures {
Self::fixup_type(cap, union_names);
}
}
_ => {}
}
}
}
impl Default for Program {
fn default() -> Self {
Self::new()
}
}
fn make_helper_word(
name: String,
inputs: &[Type],
output: Type,
body: Vec<Statement>,
source: Option<SourceLocation>,
) -> WordDef {
let mut input_stack = StackType::RowVar("a".to_string());
for ty in inputs {
input_stack = input_stack.push(ty.clone());
}
let output_stack = StackType::RowVar("a".to_string()).push(output);
WordDef {
name,
effect: Some(Effect::new(input_stack, output_stack)),
body,
source,
allowed_lints: vec![],
}
}
fn parse_type_name(name: &str) -> Type {
match name {
"Int" => Type::Int,
"Float" => Type::Float,
"Bool" => Type::Bool,
"String" => Type::String,
"Channel" => Type::Channel,
"Socket" => Type::Socket,
other => Type::Union(other.to_string()),
}
}
fn v7_renamed_to(name: &str) -> Option<&'static str> {
Some(match name {
"tcp.listen" => "net.tcp.listen",
"tcp.accept" => "net.tcp.accept",
"tcp.read" => "net.tcp.read",
"tcp.write" => "net.tcp.write",
"tcp.close" => "net.tcp.close",
"udp.bind" => "net.udp.bind",
"udp.send-to" => "net.udp.send-to",
"udp.receive-from" => "net.udp.receive-from",
"udp.close" => "net.udp.close",
"http.get" => "net.http.get",
"http.post" => "net.http.post",
"http.put" => "net.http.put",
"http.delete" => "net.http.delete",
"mod" => "i.modulo",
_ => return None,
})
}