Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ oK.js is under 1k lines of javascript, can we do the same in under 1k lines of r
RUST_LOG=rok=debug cargo run
```

## ngn/k tests

See [Tests.md](Tests.md)

## example

Expand Down
8 changes: 8 additions & 0 deletions Tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# ngn/k tests

The ngn/k tests come from here:
- https://codeberg.org/ngn/k/src/branch/master/t/t.k

test_count: 842
passed: 122
failed: 720
80 changes: 59 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub enum KW /* KWords */ {
Verb { name: String },
Adverb { name: String },
// DerivedVerb { verb: Box<KW>, adverb: String }, // TODO represent modified verbs/functions like this? eg. +/ or {2*x}'
// Projection { verb: Box<KW>, args: Vec<Option<KW>> }, // TODO
Exprs(Vec<Vec<KW>>), // list of expressions: [e1;e2;e3]
FuncArgs(Vec<Vec<KW>>), // function arguments: f[a1;a2;3]
Cond(Vec<Vec<KW>>), // conditional form $[p;t;f]
Expand Down Expand Up @@ -553,13 +554,14 @@ pub fn vec_to_list(nouns: Vec<KW>) -> Result<K> {
Ok(K::BoolArray(arr!(v)))
} else if nouns
.iter()
.all(|w| matches!(w, KW::Noun(K::Bool(_))) || matches!(w, KW::Noun(K::Int(Some(_)))))
.all(|w| matches!(w, KW::Noun(K::Bool(_))) || matches!(w, KW::Noun(K::Int(_))))
{
let v: Vec<i64> = nouns
let v: Vec<Option<i64>> = nouns
.iter()
.map(|w| match w {
KW::Noun(K::Bool(b)) => *b as i64,
KW::Noun(K::Int(Some(i))) => *i,
KW::Noun(K::Bool(b)) => Some(*b as i64),
KW::Noun(K::Int(Some(i))) => Some(*i),
KW::Noun(K::Int(None)) => None,
_ => panic!("impossible"),
})
.collect();
Expand Down Expand Up @@ -614,9 +616,12 @@ pub fn v_none3(_x: K, _y: K, _z: K) -> Result<K> { Err(RokError::Rank.into()) }
pub fn v_none4(_a: K, _b: K, _c: K, _d: K) -> Result<K> { Err(RokError::Rank.into()) }
pub fn av_none1(_env: &mut Env, _v: KW, _x: K) -> Result<K> { Err(RokError::Rank.into()) }
pub fn av_d_none2(_env: &mut Env, _v: KW, _x: K, _y: K) -> Result<K> { Err(RokError::Rank.into()) }
pub fn av_nyi3(_env: &mut Env, _v: KW, _x: KW, _y: KW) -> Result<K> { Err(RokError::NYI.into()) }

type AV1 = fn(&mut Env, KW, K) -> Result<K>;
type AV2 = fn(&mut Env, KW, K, K) -> Result<K>;
//Adverb n-adic: Noun [Verb] Adverb FuncArgs
type AVN = fn(&mut Env, KW /*Verb Adverb */, KW /* x */, KW /* y */) -> Result<K>;

type VerbDispatchTable = IndexMap<&'static str, (V1, V1, V2, V2, V2, V2, V3, V4)>;

Expand All @@ -640,7 +645,7 @@ pub fn primitives_table() -> VerbDispatchTable {
("^", (v_isnull, v_isnull, v_fill, v_except, v_fill, v_except, v_none3, v_none4)),
("#", (v_count, v_count, v_take, v_reshape, v_take, v_reshape, v_none3, v_none4,)),
("_", (v_floor, v_floor, v_drop, v_delete, v_drop, v_cut, v_none3, v_none4,)),
("$", (v_string, v_string, v_dfmt, v_dfmt, v_dfmt, v_dfmt, v_none3, v_none4,)),
("$", (v_string, v_string, v_d_dollar, v_d_dollar, v_d_dollar, v_d_dollar, v_none3, v_none4,)),
("?", (v_randfloat, v_unique, v_rand, v_find, v_rand, v_find, v_splice, v_none4,)),
("@", (v_type, v_type, v_at, v_at, v_at, v_at, v_amend3, v_amend4,)),
(".", (v_eval, v_eval, v_dot, v_dot, v_dot, v_dot, v_deepamend3, v_deepamend4,)),
Expand Down Expand Up @@ -735,14 +740,26 @@ pub fn specialcombinations_table() -> VerbDispatchTable {
])
}

pub fn adverbs_table() -> IndexMap<&'static str, (AV1, AV2)> {
// TODO: adverbs have more parse table cases than just monadic and dyadic:
// ' also has v_case, v_binarysearch, and n-adic each:
// - v_case: 0 1 0'["abc";"xyz"] / "ayc"
// - https://wiki.k-language.dev/wiki/Binary_search
// - n-adic each: {x+y-z}'[1 2 3;4 5 6;7 8 9]
//
// This will need additional cases in the parse table in eval().
// To complicate this further, case is actually n-ary:
// https://code.kx.com/q/ref/maps/#case
// q) 0 2 0'["abc";"xyz";"123";"789"]
// "a2c"
//
pub fn adverbs_table() -> IndexMap<&'static str, (AV1, AV2, AVN)> {
IndexMap::from([
("'", (v_each as AV1, v_d_each as AV2)),
("/", (a_slash, a_d_slash)), // over fixedpoint for while
("\\", (a_bslash, a_d_bslash)), // scan scan-fixedpoint scan-for scan-while
("':", (v_eachprior, v_eachprior_d_or_windows)),
("/:", (av_none1, v_d_eachright)),
("\\:", (av_none1, v_d_eachleft)),
("'", (v_each as AV1, v_d_each as AV2, v_d_quote as AVN)),
("/", (a_slash, a_d_slash, av_nyi3)), // over fixedpoint for while
("\\", (a_bslash, a_d_bslash, av_nyi3)), // scan scan-fixedpoint scan-for scan-while
("':", (v_eachprior, v_eachprior_d_or_windows, av_nyi3)),
("/:", (av_none1, v_d_eachright, av_nyi3)),
("\\:", (av_none1, v_d_eachleft, av_nyi3)),
])
}

Expand Down Expand Up @@ -811,26 +828,42 @@ pub fn apply_primitive(env: &mut Env, v: &str, l: Option<KW>, r: KW) -> Result<K
_ => Err(RokError::Error("impossible".into()).into()),
},
None => {
// adverb
let t = if v.len() < 2 {
None
} else if let Some((m_a, d_a)) = adverbs.get(&v[v.len() - 2..]) {
Some((2, (m_a, d_a)))
} else if let Some((m_a, d_a)) = adverbs.get(&v[v.len() - 1..]) {
Some((1, (m_a, d_a)))
if let Some((m_a, d_a, avn)) = adverbs.get(&v) {
Some((1, (m_a, d_a, avn)))
} else {
None
}
} else if let Some((m_a, d_a, avn)) = adverbs.get(&v[v.len() - 2..]) {
Some((2, (m_a, d_a, avn)))
} else if let Some((m_a, d_a, avn)) = adverbs.get(&v[v.len() - 1..]) {
Some((1, (m_a, d_a, avn)))
} else {
None
};
match t {
Some((adv_len, (m_a, d_a))) => match (l, r) {
Some((adv_len, (m_a, d_a, avn))) => match (l, r.clone()) {
(Some(l@KW::Noun(_)), KW::FuncArgs(_)) => {
if v.len() < 2 {
avn(env, KW::Verb{ name: v.to_string() }, l, r).map(KW::Noun)
} else {
Err(RokError::Error("NYI: Adverb case: Noun Verb Adverb FuncArgs".into()).into())
}
}
(Some(KW::Noun(l)), KW::Noun(r)) => {
d_a(env, KW::Verb { name: v[..v.len() - adv_len].to_string() }, l, r).map(KW::Noun)
d_a(env, KW::Verb { name: v[..v.len() - adv_len].to_string() }, l, r).map(KW::Noun)
}
(None, KW::Noun(r)) => {
m_a(env, KW::Verb { name: v[..v.len() - adv_len].to_string() }, r).map(KW::Noun)
}
_ => Err(RokError::Error("NYI: other adverb cases".into()).into()),
},
None => Err(RokError::Error(format!("NYI: {}", v)).into()),
None => {
println!("Todo: extra adverb cases like case and binarysearch");
println!("v: \"{v}\", l: {l:?}, r: {r}");
Err(RokError::Error(format!("NYI: {}", v)).into())
}
}
}
},
Expand Down Expand Up @@ -860,7 +893,7 @@ pub fn apply_function(env: &mut Env, f: KW, arg: KW) -> Result<KW> {
let adverbs = adverbs_table();
let adverb: &str = &adverb;
match adverbs.get(adverb) {
Some((m_a, _d_a)) => match arg {
Some((m_a, _d_a, _av3)) => match arg {
KW::Noun(x) => m_a(env, KW::Function { body, args, adverb: None }, x).map(KW::Noun),
KW::FuncArgs(_exprs) => {
Err(RokError::Error("nyi: dyad/triad/etc adverb modified functions".into()).into())
Expand Down Expand Up @@ -1166,8 +1199,13 @@ pub fn eval(env: &mut Env, sentence: Vec<KW>) -> Result<KW> {
// 4 dyad adverb (special cases, they're actually verbs)
apply_primitive(env, &name, Some(x.clone()), y.clone()).map(|r| vec![any, r])
}
(any, x @ KW::Noun(_), KW::Adverb { name }, y @ KW::FuncArgs(_)) => {
// 4 dyad adverb (more special cases, they're actually verbs)
apply_primitive(env, &name, Some(x.clone()), y.clone()).map(|r| vec![any, r])
}
// TODO: rest of the J (K is similar!) parse table (minus forks/hooks) https://www.jsoftware.com/help/jforc/parsing_and_execution_ii.htm#_Toc191734587
(KW::LP, w, KW::RP, any) => Ok(vec![w.clone(), any.clone()]), // 8 paren
(KW::LP, KW::RP, any1, any2 ) => Ok(vec![KW::Noun(K::List(vec![])), any1.clone(), any2.clone()]),
(KW::LP, KW::Noun(n1), KW::SC, KW::Noun(n2)) => {
// List
if let Some(i) = stack.iter().position(|w| matches!(w, KW::RP)) {
Expand Down
73 changes: 69 additions & 4 deletions src/verbs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ pub fn v_equal(x: K, y: K) -> Result<K> {
(K::Int(Some(l)), K::Int(Some(r))) => Ok(K::Bool((l == r) as u8)),
(K::Int(None), K::Int(_)) | (K::Int(_), K::Int(None)) => Ok(K::Bool(0)),
(K::Float(l), K::Float(r)) => Ok(K::Bool((l == r) as u8)),
(K::Symbol(l), K::Symbol(r)) => Ok(K::Bool((l == r) as u8)),
(K::BoolArray(l), K::BoolArray(r)) => Ok(K::BoolArray(l.equal(&r).unwrap().into())),
(K::IntArray(l), K::IntArray(r)) => Ok(K::BoolArray(l.equal(&r).unwrap().into())),
(K::FloatArray(l), K::FloatArray(r)) => Ok(K::BoolArray(l.equal(&r).unwrap().into())),
(K::CharArray(l), K::CharArray(r)) => {
Ok(K::BoolArray(arr!(l.chars().zip(r.chars()).map(|(l, r)| l == r).collect::<Vec<bool>>())))
}
(K::SymbolArray(l), K::SymbolArray(r)) => Ok(K::BoolArray(l.equal(&r).unwrap().into())),
(K::List(l), K::List(r)) => Ok(K::BoolArray(arr!(zip(l.iter(), r.iter())
.map(|(l, r)| {
let (l, r) = promote_nouns(l.clone(), r.clone());
Expand Down Expand Up @@ -318,12 +320,20 @@ macro_rules! atomicdyad {
(l, K::List(rv)) => {
Ok(K::List(rv.iter().map(|y| $v(l.clone(), y.clone()).unwrap()).collect()))
}
(l@K::Char(_) | l@K::CharArray(_), r@K::Char(_) | r@K::CharArray(_)) => len_ok(&l, &r).and_then(|_| Ok(v_cast(K::Symbol("i".into()), l)? $op v_cast(K::Symbol("i".into()), r)?)),
(l, r@K::Char(_) | r@K::CharArray(_)) => len_ok(&l, &r).and_then(|_| Ok(l $op v_cast(K::Symbol("i".into()), r)?)),
(l@K::Char(_) | l@K::CharArray(_), r) => len_ok(&l, &r).and_then(|_| Ok(v_cast(K::Symbol("i".into()), l)? $op r)),
(l,r) => len_ok(&l, &r).and_then(|_| Ok(l $op r)),
}
};
}
pub fn v_plus(l: K, r: K) -> Result<K> { atomicdyad!(+, v_plus, add, l, r) }
pub fn v_negate(x: K) -> Result<K> { Ok(K::Int(Some(-1i64)) * x) }
pub fn v_negate(x: K) -> Result<K> {
match x {
K::Char(_) | K::CharArray(_) => Ok(K::Int(Some(-1i64)) * v_cast(K::Symbol("i".into()), x)?),
_ => Ok(K::Int(Some(-1i64)) * x),
}
}
pub fn v_minus(l: K, r: K) -> Result<K> { atomicdyad!(-, v_minus, sub, l, r) }

pub fn v_first(x: K) -> Result<K> {
Expand Down Expand Up @@ -734,7 +744,24 @@ pub fn v_greater(x: K, y: K) -> Result<K> {
}

pub fn v_not(_r: K) -> Result<K> { Err(RokError::NYI.into()) }
pub fn v_match(_l: K, _r: K) -> Result<K> { Err(RokError::NYI.into()) }
pub fn v_match(x: K, y: K) -> Result<K> {
match (x, y) {
(K::Bool(l), K::Bool(r)) => Ok(K::Bool((l == r) as u8)),
(K::Int(l), K::Int(r)) => Ok(K::Bool((l == r) as u8)),
(K::Float(l), K::Float(r)) => Ok(K::Bool((l == r) as u8)),
(K::Char(l), K::Char(r)) => Ok(K::Bool((l == r) as u8)),
(K::Symbol(l), K::Symbol(r)) => Ok(K::Bool((l == r) as u8)),
(K::SymbolArray(l), K::SymbolArray(r)) => Ok(K::Bool((l == r) as u8)),
(K::BoolArray(l), K::BoolArray(r)) => Ok(K::Bool((l == r) as u8)),
(K::IntArray(l), K::IntArray(r)) => Ok(K::Bool((l == r) as u8)),
(K::FloatArray(l), K::FloatArray(r)) => Ok(K::Bool((l == r) as u8)),
(K::CharArray(l), K::CharArray(r)) => Ok(K::Bool((l == r) as u8)),
(K::List(l), K::List(r)) => Ok(K::Bool((l == r) as u8)),
(K::Dictionary(l), K::Dictionary(r)) => Ok(K::Bool((l == r) as u8)),
(K::Table(l), K::Table(r)) => Ok(K::Bool((l == r) as u8)),
_ => Err(RokError::NYI.into()),
}
}

pub fn v_enlist(x: K) -> Result<K> {
match x {
Expand Down Expand Up @@ -827,12 +854,26 @@ pub fn v_delete(_l: K, _r: K) -> Result<K> { Err(RokError::NYI.into()) }
pub fn v_cut(_l: K, _r: K) -> Result<K> { Err(RokError::NYI.into()) }

pub fn v_string(_r: K) -> Result<K> { Err(RokError::NYI.into()) }
pub fn v_d_dollar(l: K, r: K) -> Result<K> {
match l {
K::Symbol(_) => v_cast(l, r),
_ => Err(RokError::NYI.into()),
}
}
pub fn v_dfmt(_l: K, _r: K) -> Result<K> { Err(RokError::NYI.into()) }
pub fn v_pad(_l: K, _r: K) -> Result<K> { Err(RokError::NYI.into()) }
pub fn v_cast(l: K, _r: K) -> Result<K> {
pub fn v_cast(l: K, r: K) -> Result<K> {
match l {
K::Symbol(s) if s == "c" => Err(RokError::Error("nyi: cast to string".into()).into()),
K::Symbol(s) if s == "i" => Err(RokError::Error("nyi: cast to int".into()).into()),
K::Symbol(s) if s == "i" => match r {
K::Char(c) => Ok(K::Int(Some((c as u32) as i64))),
K::CharArray(s) => Ok(K::IntArray(arr!(s
.chars()
.into_iter()
.map(|i| (i as u32) as i64)
.collect::<Vec<i64>>()))),
_ => Err(RokError::Error("nyi: cast to int".into()).into()),
},
K::Symbol(s) if s == "f" => Err(RokError::Error("nyi: cast to float".into()).into()),
_ => Err(RokError::Type.into()),
}
Expand Down Expand Up @@ -1221,6 +1262,30 @@ pub fn v_d_bang(l: K, r: K) -> Result<K> {
}
}

pub fn v_d_quote(_env: &mut Env, _v: KW, x: KW, y: KW) -> Result<K> {
// dispatch dyadic verb form of '
match (x, y) {
// (KW::Noun(K::IntArray(_)), KW::FuncArgs(_)) => todo!("todo"),
_ => Err(RokError::Error("v_d_quote(). Dispatch of dyadic ' ".into()).into()),
}
}
pub fn v_binarysearch(_l: K, _r: K) -> Result<K> { Err(RokError::NYI.into()) }
pub fn v_case(_l: K, _r: K) -> Result<K> {
// https://code.kx.com/q/ref/maps/#case
// https://codeberg.org/ngn/k/commit/319fcaa609e2099c33f04d5373393df46fe78810
// 0 1 0'["abc";"xyz"] / "ayc"
// Err(RokError::NYI.into())
todo!("AA TODO nyi")
}

pub fn v_quote(env: &mut Env, v: KW, x: K) -> Result<K> {
// dispatch adverb ' variants
match v {
KW::Verb { .. } | KW::Function { .. } => v_each(env, v, x),
KW::Nothing => todo!(),
_ => Err(RokError::NYI.into()),
}
}
pub fn v_each(env: &mut Env, v: KW, x: K) -> Result<K> {
match v {
f @ KW::Verb { .. } | f @ KW::Function { .. } => {
Expand Down
10 changes: 10 additions & 0 deletions tests/aoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,13 @@ fn test_aoc2015_12_03() {
// let expected = eval(&mut env, scan("1234").unwrap()).unwrap(); // 4403 too high
// assert_eq!(res, expected);
}

#[test]
fn test_aoc2025_12_01() {
let mut env = Env { names: HashMap::new(), parent: None };

let s = r#"+/0=50{100!x+y}\{.(" -"@"L"=*x),1_x}'x:("L68";"L30";"R48";"L5";"R60";"L55";"L1";"L99";"R14";"L82")"#;
let res = eval(&mut env, scan(s).unwrap()).unwrap();
let expected = eval(&mut env, scan("3").unwrap()).unwrap();
assert_eq!(res, expected);
}
20 changes: 16 additions & 4 deletions tests/ngnk.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use std::io::Write;
use roklang::*;
use std::collections::HashMap;
use std::fs::File;
Expand Down Expand Up @@ -51,10 +52,11 @@ fn test_ngnk_tests() {
failed_tests += 1;
println!("\nskipping line {} known failure: {}", i + 1, l);
} else {
println!("\nline {}: {}", i + 1, l);
// println!("\nline {}: {}", i + 1, l);
let t: Vec<&str> = l.split(" / ").collect();
if t.len() != 2 {
println!("Skipping dud line: {}", l);
if l.chars().next() == Some('/') || t.len() != 2 {
// comment or other
// println!("Skipping dud line: {}", l);
} else {
test_count += 1;
// assert_eq!(k_eval(t[0]), k_eval(t[1]));
Expand All @@ -66,7 +68,11 @@ fn test_ngnk_tests() {
};
if fail {
failed_tests += 1;
println!("Failed test: ({failed_tests}/{test_count}): {}", l);
println!("Failed test: line {} ({failed_tests}/{test_count}): {}", i, l);
// Useful for debugging:
// if failed_tests > 10 {
// panic!("More than {failed_tests} failures: bailing out");
// }
match res_l {
Ok(k) => println!("{}", k),
Err(_) => println!("{:?}", res_l),
Expand All @@ -78,5 +84,11 @@ fn test_ngnk_tests() {
}
}
println!("\ntest_count: {}\npassed: {}\nfailed: {}", test_count, passed_tests, failed_tests);

let mut w = File::create("Tests.md").unwrap();
writeln!(&mut w, "# ngn/k tests\n").unwrap();
writeln!(&mut w, "The ngn/k tests come from here: \n\t- https://codeberg.org/ngn/k/src/branch/master/t/t.k").unwrap();
writeln!(&mut w, "\ntest_count: {}\npassed: {}\nfailed: {}", test_count, passed_tests, failed_tests).unwrap();

assert!(failed_tests == 0);
}
Loading