release 0.1.0

This commit is contained in:
rus07tam 2026-05-06 12:21:06 +03:00
commit 30d94536a9
90 changed files with 7722 additions and 0 deletions

17
modules/core/assert.owa Normal file
View file

@ -0,0 +1,17 @@
(namespace assert
(defmacro ok! [] (if (bool.and $%&)
:true
(throw! "assertion failed")))
(defmacro not! [] (if (bool.and $%&)
(throw! "assertion failed")
:true))
(defmacro eq! [] (if-eq $(%&)
:true
(throw! "assertion failed")))
(defmacro nq! [] (if-eq $(%&)
(throw! "assertion failed")
:true))
)

10
modules/core/ast.owa Normal file
View file

@ -0,0 +1,10 @@
(namespace ast
(defmacro pack! [v]
(builtins.ast.pack $v))
(defmacro value! [v]
(map.get (ast.pack! $v) :data))
(defmacro type! [v]
(map.get (ast.pack! $v) :type))
)

12
modules/core/branch.owa Normal file
View file

@ -0,0 +1,12 @@
(seq
;; builtins
(def if-eq builtins.cond.if_eq)
(def match builtins.cond.match)
(def if-has builtins.cond.if_has)
(defmacro if [cond then]
(if-eq (:false $cond) (seq $%&) (seq $then)))
(defmacro unless [cond then]
(if-eq (:false $cond) (seq $then) (seq $%&)))
)

25
modules/core/cmp.owa Normal file
View file

@ -0,0 +1,25 @@
(seq
(defmacro eq? <Bool>[]
(if-eq ($%%) :true :false))
(defmacro nq? <Bool>[]
(if-eq ($%%) :false :true))
(fn cmp <Keyword>[left right]
(match (builtins.cmp left right)
(-1 :less)
(0 :equal)
(1 :greater)))
(fn lt? <Bool>[left right]
(eq? (cmp left right) :less))
(fn lte? <Bool>[left right]
(bool.or (lt? left right) (eq? left right)))
(fn gt? <Bool>[left right]
(eq? (cmp left right) :greater))
(fn gte? <Bool>[left right]
(bool.or (gt? left right) (eq? left right)))
)

22
modules/core/error.owa Normal file
View file

@ -0,0 +1,22 @@
(seq
(fn throw! []
(throw.runtime_error! (str.join "" %%)))
(fn panic! [v]
(throw.panic! v))
(fn unreachable! []
(throw! "Reached unreachable code"))
(fn not_implemented! []
(throw! "Not implemented"))
(def try builtins.errors.try)
(defmacro catch [pattern then]
(lambda [msg type] (if-eq (type $pattern)
$then
:null)))
(namespace throw
(def panic! builtins.errors.throw_panic)
(def type_error! builtins.errors.throw_type_error)
(def arity_error! builtins.errors.throw_arity_error)
(def runtime_error! builtins.errors.throw_runtime_error)
))

32
modules/core/ffi.owa Normal file
View file

@ -0,0 +1,32 @@
(namespace ffi
(def unload builtins.ffi.unload)
(def ctypes [
:i8 :i16 :i32 :i64
:u8 :u16 :u32 :u64
:f32 :f64
:ptr
:str
])
(defmacro extern [name libname]
(namespace $name
(builtins.ffi.load $libname)
(fn call [symbol signature args]
(builtins.ffi.call $libname symbol signature args)
)
)
)
(defmacro native [name libname]
(namespace $name
(builtins.ffi.load $libname)
(defmacro fn [name symbol args ret]
(def '$name #(builtins.ffi.call $libname '$symbol ['$args '$ret] %&))
)
$%&
)
)
(extern ucrtbase "ucrtbase.dll")
)

8
modules/core/lambda.owa Normal file
View file

@ -0,0 +1,8 @@
(seq
;; builtins
(def apply builtins.apply)
(def lambda builtins.lambda)
(defmacro fn [name params body]
(def $name (lambda $params $body)))
)

23
modules/core/loop.owa Normal file
View file

@ -0,0 +1,23 @@
(seq
(def break builtins.flow.break)
(def continue builtins.flow.continue)
(def loop builtins.flow.loop)
(defmacro return []
(builtins.flow.return $%% null))
(defmacro while [cond] (loop
(if (eq? $cond :false) (break) :null)
$%&))
(defmacro do-while [cond] (loop
$%&
(if (eq? $cond :false) (break) :null)))
(defmacro for [var iter] (vec.map
(lambda [x] (seq
(def $var x)
$%&))
(vec.from $iter)
))
)

25
modules/core/main.owa Normal file
View file

@ -0,0 +1,25 @@
(builtins.seq
(builtins.def include builtins.include)
(include "scope")
(include "branch")
(include "lambda")
(def exec builtins.exec)
(def trace builtins.trace)
(def typeof builtins.typeof)
(fn identity [x] x)
(include "types")
(include "assert")
(include "ast")
(include "cmp")
(include "error")
(include "ffi")
(include "loop")
(include "math")
(include "test")
(test.space "core" (include "tests/main"))
)

14
modules/core/math.owa Normal file
View file

@ -0,0 +1,14 @@
(seq
(def + builtins.math.add)
(def - builtins.math.sub)
(def * builtins.math.mul)
(def / builtins.math.div)
(def % builtins.math.mod)
(def ** builtins.math.pow)
(fn ++ <Number>[<Number>x]
(+ x 1))
(fn -- <Number>[<Number>x]
(- x 1))
)

22
modules/core/scope.owa Normal file
View file

@ -0,0 +1,22 @@
(builtins.seq
;; builtins
(builtins.def def builtins.def)
(def lookup builtins.lookup)
(def set! builtins.set!)
(def scope builtins.scope)
;; macro
(def macro builtins.macro)
(def defmacro (macro [name params body]
(def $name (macro $params $body))))
;; flow
(defmacro seq []
(builtins.seq :null $%&))
(defmacro namespace [name] (def $name (scope $%&)))
;; other
(defmacro mut! [v fn] (seq
(set! (ast.value! $v) ($fn $v $%&)))
)
)

16
modules/core/test.owa Normal file
View file

@ -0,0 +1,16 @@
(namespace test
(def target (lookup __test__ null))
(defmacro if-target [then]
(if-eq (test.target null) :null (seq $%%)))
(defmacro case [name] (test.if-target
(builtins.errors.try
(scope $%& (trace "Running test \"" $name "\": OK ✅"))
#(trace "Running test \"" $name "\": FAILED ❌ (" %2 ": " %1 ")"))))
(defmacro space [name] (if
(bool.or (eq? test.target $name) (eq? test.target "."))
(namespace $name (seq $%&))
))
)

View file

@ -0,0 +1,38 @@
(namespace tests.bool
(test.case "bool.bool?"
(assert.ok! (bool? true))
(assert.ok! (bool? false))
(assert.not! (bool? 1))
)
(test.case "bool.not"
(assert.eq! (bool.not true) false)
(assert.eq! (bool.not false) true)
)
(test.case "bool.and"
(assert.ok! (bool.and true true))
(assert.not! (bool.and true false))
(assert.not! (bool.and false true))
(assert.not! (bool.and false false))
)
(test.case "bool.or"
(assert.ok! (bool.or true true))
(assert.ok! (bool.or true false))
(assert.ok! (bool.or false true))
(assert.not! (bool.or false false))
)
(test.case "bool.nand"
(assert.not! (bool.nand true true))
(assert.ok! (bool.nand true false))
(assert.ok! (bool.nand false true))
(assert.ok! (bool.nand false false))
)
(test.case "bool.new"
(assert.eq! (bool.new true) true)
(assert.eq! (bool.new false) false)
)
)

View file

@ -0,0 +1,41 @@
(namespace tests.cmp
(test.case "cmp.eq?"
(assert.ok! (eq? 1 1))
(assert.not! (eq? 1 2))
)
(test.case "cmp.nq?"
(assert.ok! (nq? 1 2))
(assert.not! (nq? 1 1))
)
(test.case "cmp.cmp"
(assert.eq! (cmp 1 2) :less)
(assert.eq! (cmp 2 1) :greater)
(assert.eq! (cmp 1 1) :equal)
)
(test.case "cmp.lt?"
(assert.ok! (lt? 1 2))
(assert.not! (lt? 2 1))
(assert.not! (lt? 1 1))
)
(test.case "cmp.lte?"
(assert.ok! (lte? 1 2))
(assert.ok! (lte? 1 1))
(assert.not! (lte? 2 1))
)
(test.case "cmp.gt?"
(assert.ok! (gt? 2 1))
(assert.not! (gt? 1 2))
(assert.not! (gt? 1 1))
)
(test.case "cmp.gte?"
(assert.ok! (gte? 2 1))
(assert.ok! (gte? 1 1))
(assert.not! (gte? 1 2))
)
)

View file

@ -0,0 +1,42 @@
(namespace tests.loop
(test.case "while.basic-true"
(def counter 0)
(while (eq? counter 0)
(set! counter 1))
(assert.eq! counter 1)
)
(test.case "while.condition-false"
(def counter 5)
(while (lt? counter 0)
(set! counter (+ counter 1)))
(assert.eq! counter 5)
)
(test.case "while.counting-loop"
(def n 0)
(while (lt? n 10)
(set! n (+ n 1)))
(assert.eq! n 10)
)
(test.case "while.loop-condition-reached"
(def i 0)
(while (lt? i 5)
(set! i (+ i 1)))
(assert.eq! i 5)
)
(test.case "while.nested"
(def outer 0)
(def inner 0)
(def total 0)
(while (lt? outer 3)
(set! inner 0)
(while (lt? inner 2)
(set! total (+ total 1))
(set! inner (+ inner 1)))
(set! outer (+ outer 1)))
(assert.eq! total 6)
)
)

View file

@ -0,0 +1,14 @@
(seq
(include "vec")
(include "str")
(include "set")
(include "map")
(include "bool")
(include "option")
(include "scope")
(include "null")
(include "cmp")
(include "math")
(include "types")
(include "loop")
)

View file

@ -0,0 +1,83 @@
(namespace tests.map
(test.case "map.len"
(assert.eq! (map.len (map.new)) 0)
)
(test.case "map.empty?"
(assert.ok! (map.empty? (map.new)))
)
(test.case "map.is-empty?"
(assert.ok! (map.is-empty? (map.new)))
)
(test.case "map.get"
(seq
(def m (map.new :a 1 :b 2))
(assert.eq! (map.get m :a) 1)
(assert.eq! (map.get m :b) 2)
(assert.eq! (map.get m :c) null)
)
)
(test.case "map.put"
(seq
(def m (map.new :a 1))
(def m2 (map.put m :b 2))
(assert.eq! (map.len m2) 2)
(assert.eq! (map.get m2 :b) 2)
)
)
(test.case "map.has-key?"
(seq
(def m (map.new :a 1 :b 2))
(assert.ok! (map.has-key? m :a))
(assert.not! (map.has-key? m :c))
)
)
(test.case "map.has-value?"
(seq
(def m (map.new :a 1 :b 2))
(assert.ok! (map.has-value? m 1))
(assert.not! (map.has-value? m 5))
)
)
(test.case "map.keys"
(seq
(def m (map.new :a 1 :b 2))
(def ks (map.keys m))
(assert.eq! (vec.len ks) 2)
)
)
(test.case "map.values"
(seq
(def m (map.new :a 1 :b 2))
(def vs (map.values m))
(assert.eq! (vec.len vs) 2)
)
)
(test.case "map.merge"
(seq
(def m1 (map.new :a 1 :b 2))
(def m2 (map.new :b 20 :c 3))
(def m3 (map.merge m1 m2))
(assert.eq! (map.get m3 :a) 1)
(assert.eq! (map.get m3 :b) 20)
(assert.eq! (map.get m3 :c) 3)
)
)
(test.case "map.dissoc"
(seq
(def m (map.new :a 1 :b 2 :c 3))
(def m2 (map.dissoc m :b))
(assert.eq! (map.len m2) 2)
(assert.not! (map.has-key? m2 :b))
)
)
)

View file

@ -0,0 +1,40 @@
(namespace tests.math
(test.case "math.add"
(assert.eq! (+ 1 2) 3)
(assert.eq! (+ 0 0) 0)
)
(test.case "math.sub"
(assert.eq! (- 5 3) 2)
(assert.eq! (- 1 1) 0)
)
(test.case "math.mul"
(assert.eq! (* 3 4) 12)
(assert.eq! (* 0 5) 0)
)
(test.case "math.div"
(assert.eq! (/ 10 2) 5)
)
(test.case "math.mod"
(assert.eq! (% 10 3) 1)
(assert.eq! (% 5 5) 0)
)
(test.case "math.pow"
(assert.eq! (** 2 3) 8)
(assert.eq! (** 5 2) 25)
)
(test.case "math.++"
(assert.eq! (++ 5) 6)
(assert.eq! (++ 0) 1)
)
(test.case "math.--"
(assert.eq! (-- 5) 4)
(assert.eq! (-- 0) -1)
)
)

View file

@ -0,0 +1,8 @@
(namespace tests.null
(test.case "null.null?"
(assert.ok! (null? null))
(assert.ok! (null? :nil))
(assert.not! (null? 0))
(assert.not! (null? false))
)
)

View file

@ -0,0 +1,33 @@
(namespace tests.option
(test.case "option.ok"
(seq
(def opt (option.ok 42))
(assert.ok! (option.ok? opt))
(assert.eq! (vec.get opt 1) 42)
)
)
(test.case "option.err"
(seq
(def opt (option.err "error"))
(assert.ok! (option.err? opt))
(assert.eq! (vec.get opt 1) "error")
)
)
(test.case "option.ok?"
(assert.ok! (option.ok? (option.ok 1)))
(assert.not! (option.ok? (option.err "x")))
)
(test.case "option.err?"
(assert.ok! (option.err? (option.err "x")))
(assert.not! (option.err? (option.ok 1)))
)
(test.case "option.option?"
(assert.ok! (option? (option.ok 1)))
(assert.ok! (option? (option.err "x")))
(assert.not! (option? [1 2]))
)
)

View file

@ -0,0 +1,78 @@
(namespace tests.scopes
(test.case "def"
(def x 1)
(assert.eq! x 1)
(def [y z] [1 2])
(assert.eq! y 1)
(assert.eq! z 2)
)
(test.case "def bounded"
(def x 1)
(try
(def x 2)
(catch :Panic (return))
)
(unreachable!)
)
(test.case "set!"
(def [x y] [1 1])
(set! x 2)
(assert.eq! x 2)
(set! [x y] [3 4])
(assert.eq! x 3)
(assert.eq! y 4)
)
(test.case "set! unbounded"
(try
(set! x 2)
(catch :UnboundVariable (return))
)
(unreachable!)
)
(test.case "lookup"
(def x 1)
(assert.eq! (lookup x null) 1)
(assert.eq! (lookup y null) null)
)
(test.case "inheritance"
(scope
(def x 1)
(scope
(assert.eq! x 1)
(set! x 2)
)
(assert.eq! x 2)
)
)
(test.case "shadowing"
(scope
(def x 100)
(scope
(def x 1)
)
(assert.eq! x 100)
)
(assert.eq! (lookup x null) null)
)
(test.case "leakage"
(scope (def x 1))
(assert.eq! (lookup x null) null)
(scope (def y 2))
(assert.eq! (lookup y null) null)
(scope
(scope (def z 1))
(assert.eq! (lookup z null) null)
)
)
)

View file

@ -0,0 +1,44 @@
(namespace tests.set
(test.case "set.empty?"
(assert.ok! (set.empty? (set.new)))
(assert.not! (set.empty? (set.new 1)))
)
(test.case "set.len"
(assert.eq! (set.len (set.new)) 0)
(assert.eq! (set.len (set.new 1)) 1)
(assert.eq! (set.len (set.new 1 2 3)) 3)
)
(test.case "set.has"
(assert.ok! (set.has (set.new 1 2 3) 2))
(assert.not! (set.has (set.new 1 2 3) 5))
)
(test.case "set.add"
(assert.ok! (set.has (set.add (set.new 1 2) 3) 3))
(assert.eq! (set.len (set.add (set.new 1 2) 3)) 3)
)
(test.case "set.remove"
(assert.not! (set.has (set.remove (set.new 1 2 3) 2) 2))
(assert.eq! (set.len (set.remove (set.new 1 2 3) 2)) 2)
)
(test.case "set.union"
(assert.eq! (set.len (set.union (set.new 1 2) (set.new 2 3))) 3)
(assert.ok! (set.has (set.union (set.new 1 2) (set.new 2 3)) 1))
(assert.ok! (set.has (set.union (set.new 1 2) (set.new 2 3)) 3))
)
(test.case "set.intersection"
(assert.eq! (set.len (set.intersection (set.new 1 2 3) (set.new 2 3 4))) 2)
(assert.ok! (set.has (set.intersection (set.new 1 2 3) (set.new 2 3 4)) 2))
(assert.ok! (set.has (set.intersection (set.new 1 2 3) (set.new 2 3 4)) 3))
)
(test.case "set.difference"
(assert.eq! (set.len (set.difference (set.new 1 2 3) (set.new 2 3))) 1)
(assert.ok! (set.has (set.difference (set.new 1 2 3) (set.new 2 3)) 1))
)
)

View file

@ -0,0 +1,67 @@
(namespace tests.str
(test.case "str.len"
(assert.eq! (str.len "") 0)
(assert.eq! (str.len "hello") 5)
)
(test.case "str.empty?"
(assert.ok! (str.empty? ""))
(assert.not! (str.empty? "a"))
)
(test.case "str.is-empty?"
(assert.ok! (str.is-empty? ""))
(assert.not! (str.is-empty? "a"))
)
(test.case "str.first"
(assert.eq! (str.first "hello") "h")
)
(test.case "str.last"
(assert.eq! (str.last "hello") "o")
)
(test.case "str.concat"
(assert.eq! (str.concat "hello" " " "world") "hello world")
)
(test.case "str.substring"
(assert.eq! (str.substring "hello" 0 3) "hel")
(assert.eq! (str.substring "hello" 1 4) "ell")
)
(test.case "str.take"
(assert.eq! (str.take "hello" 3) "hel")
)
(test.case "str.drop"
(assert.eq! (str.drop "hello" 2) "llo")
)
(test.case "str.reverse"
(assert.eq! (str.reverse "hello") "olleh")
)
(test.case "str.starts-with?"
(assert.ok! (str.starts-with? "hello" "hel"))
(assert.not! (str.starts-with? "hello" "xyz"))
)
(test.case "str.ends-with?"
(assert.ok! (str.ends-with? "hello" "lo"))
(assert.not! (str.ends-with? "hello" "xyz"))
)
(test.case "str.split"
(assert.eq! (str.split "hello world" " ") ["hello" "world"])
(assert.eq! (str.split "hello world" "w") ["hello " "orld"])
(assert.eq! (str.split "hello world" "x") ["hello world"])
)
(test.case "str.join"
(assert.eq! (str.join " " ["hello" "world"]) "hello world")
(assert.eq! (str.join "w" ["hello " "orld"]) "hello world")
(assert.eq! (str.join "x" ["hello world"]) "hello world")
)
)

View file

@ -0,0 +1,53 @@
(namespace tests.types
(test.case "types.is?"
(assert.ok! (is? 42 :int))
(assert.ok! (is? "hello" :str))
(assert.ok! (is? [] :vec))
(assert.not! (is? 42 :str))
)
(test.case "types.guard"
(seq
(def int-guard (guard :int))
(assert.ok! (int-guard 42))
(assert.not! (int-guard "42"))
)
)
(test.case "types.kw?"
(assert.ok! (kw? :keyword))
(assert.not! (kw? "not-keyword"))
)
(test.case "types.type?"
(assert.ok! (type? :int))
(assert.ok! (type? :str))
(assert.not! (type? :unknown))
)
(test.case "types.str?"
(assert.ok! (str? "hello"))
(assert.not! (str? 42))
)
(test.case "types.vec?"
(assert.ok! (vec? []))
(assert.ok! (vec? [1 2 3]))
(assert.not! (vec? 42))
)
(test.case "types.bool?"
(assert.ok! (bool? true))
(assert.ok! (bool? false))
(assert.not! (bool? 1))
)
(test.case "types.null?"
(assert.ok! (null? null))
(assert.not! (null? 0))
)
(test.case "types.map.is?"
(assert.ok! (map.is? (map.new)))
)
)

View file

@ -0,0 +1,91 @@
(namespace tests.vec
(test.case "vec.empty?"
(assert.ok! (vec.empty? []))
(assert.not! (vec.empty? [1]))
)
(test.case "vec.len"
(assert.eq! (vec.len []) 0)
(assert.eq! (vec.len [1]) 1)
(assert.eq! (vec.len [1 2 3]) 3)
)
(test.case "vec.first"
(assert.eq! (vec.first [1 2 3]) 1)
(assert.eq! (vec.first ["a"]) "a")
)
(test.case "vec.last"
(assert.eq! (vec.last [1 2 3]) 3)
(assert.eq! (vec.last ["x"]) "x")
)
(test.case "vec.get"
(assert.eq! (vec.get [10 20 30] 0) 10)
(assert.eq! (vec.get [10 20 30] 1) 20)
(assert.eq! (vec.get [10 20 30] 2) 30)
)
(test.case "vec.map"
(assert.eq!
(vec.map (lambda [x _] (+ x 1)) [1 2 3])
[2 3 4])
)
(test.case "vec.filter"
(assert.eq!
(vec.filter (lambda [x _] (gt? x 1)) [1 2 3])
[2 3])
)
(test.case "vec.has"
(assert.ok! (vec.has [1 2 3] 2))
(assert.not! (vec.has [1 2 3] 5))
)
(test.case "vec.includes?"
(assert.ok! (vec.includes? [1 2 3] 2))
(assert.not! (vec.includes? [1 2 3] 5))
)
(test.case "vec.find"
(assert.eq!
(vec.find (lambda [x _] (gt? x 2)) [1 2 3 4])
3)
)
(test.case "vec.reverse"
(assert.eq! (vec.reverse [1 2 3]) [3 2 1])
)
(test.case "vec.take"
(assert.eq! (vec.take [1 2 3 4 5] 3) [1 2 3])
)
(test.case "vec.drop"
(assert.eq! (vec.drop [1 2 3 4 5] 2) [3 4 5])
)
(test.case "vec.uniq"
(assert.eq! (vec.uniq [1 2 2 3 3 3]) [1 2 3])
)
(test.case "vec.every?"
(assert.ok! (vec.every? (lambda [x _] (gt? x 0)) [1 2 3]))
(assert.not! (vec.every? (lambda [x _] (gt? x 1)) [1 2 3]))
)
(test.case "vec.some?"
(assert.ok! (vec.some? (lambda [x _] (gt? x 2)) [1 2 3]))
(assert.not! (vec.some? (lambda [x _] (gt? x 5)) [1 2 3]))
)
(test.case "vec.count"
(assert.eq! (vec.count (lambda [x _] (gt? x 1)) [1 2 3]) 2)
)
(test.case "vec.sum"
(assert.eq! (vec.sum [1 2 3]) 6)
(assert.eq! (vec.sum []) 0)
)
)

View file

@ -0,0 +1,23 @@
(seq
(def true :true)
(def false :false)
(fn bool? [x]
(bool.or (eq? x true) (eq? x false)))
(namespace bool
(fn new [x]
(eq? x true))
(def __call__ new)
(defmacro or []
(if-eq (false $%%) false true))
(defmacro and []
(if-has false ($%%) false true))
(defmacro nand []
(if-has false ($%%) true false))
(fn not [x]
(if-eq (x true) false true))
))

View file

@ -0,0 +1,39 @@
(seq
(def types [
:symbol
:keyword
:str
:float
:int
:vec
:call
:set
:map
:quote
:lambda
:macro
])
(fn is? [x type]
(eq? (typeof x) type))
(fn guard [type]
(lambda [x] (is? x type)))
(def kw? (guard :keyword))
(def kw builtins.str.kw)
(include "bool")
(include "map")
(include "null")
(include "option")
(include "set")
(include "str")
(include "struct")
(include "vec")
(def vec? (guard :vec))
(def map? (guard :map))
(fn type? [x]
(vec.has types x))
)

View file

@ -0,0 +1,94 @@
(namespace map
(def type :map)
(def is? (guard type))
(def new builtins.map.new)
(def __call__ new)
(fn keys [m]
(vec.map #(vec.first %1) (vec.from m)))
(fn values [m]
(vec.map #(vec.last %1) (vec.from m)))
(fn len [m]
(vec.len (vec.from m)))
(fn empty? [m]
(eq? (len m) 0))
(fn is-empty? [m]
(empty? m))
(fn get [m key]
(vec.fold
(lambda [result pair _]
(if (eq? (vec.get pair 0) key)
(vec.get pair 1)
result))
null
(vec.from m)))
(fn put [m key value]
(apply apply new (vec.from m) [[key value]]))
(fn assoc [m key value]
(put m key value))
(fn dissoc [m key]
(apply apply new (vec.filter
(lambda [pair _] (nq?
(vec.get pair 0)
key
))
(vec.from m))
))
(fn has-key? [m key]
(if (null? (get m key))
false
true))
(fn has-value? [m value]
(vec.fold
(lambda [result pair _]
(seq
(def k (vec.get pair 0))
(def v (vec.get pair 1))
(bool.or result (eq? v value))))
false
(vec.from m)))
(fn map [callback m]
(apply new (vec.apply-flat (vec.map
(lambda [pair _]
(seq
(def k (vec.get pair 0))
(def v (vec.get pair 1))
(def new-v (callback v k))
[k new-v]))
(vec.from m)))))
(fn filter [callback m]
(apply new (vec.apply-flat (vec.filter
(lambda [pair _]
(seq
(def k (vec.get pair 0))
(def v (vec.get pair 1))
(callback v k)))
(vec.from m)))))
(fn merge [m1 m2]
(apply apply new (vec.from m1) (vec.from m2)))
(fn merge-all []
(vec.fold
(lambda [acc m _]
(merge acc m))
(new)
%%))
(fn conj [m other]
(if (map.is? other)
(merge m other)
m))
)

View file

@ -0,0 +1,5 @@
(seq
(def null :null)
(fn null? [x]
(bool.or (eq? x null) (eq? x :nil)))
)

View file

@ -0,0 +1,26 @@
(seq
(namespace option
(fn ok <Option>[value]
(vec :ok value))
(fn ok? <Bool>[x]
(bool.and
(vec? x)
(eq? (vec.first x) :ok)))
(fn err <Option>[value]
(vec :err value))
(fn err? <Bool>[x]
(bool.and
(vec? x)
(eq? (vec.first x) :err)))
)
(fn option? [x]
(bool.and
(vec? x)
(bool.or
(eq? (vec.first x) :err)
(eq? (vec.first x) :ok))))
)

View file

@ -0,0 +1,60 @@
(seq
(namespace set
(def type :set)
(def is? (guard type))
(fn new [] (apply from %%))
(def __call__ new)
(def fold builtins.set.fold)
(def from builtins.set.from)
(def empty #{})
(fn empty? <Bool>[s]
(eq? s empty))
(fn len <Int>[<Set>s]
(fold (lambda [acc _] (+ acc 1)) 0 s))
(fn has <Bool>[<Set>s item]
(fold
(lambda [acc elem]
(bool.or acc (eq? elem item)))
false
s))
(fn add <Set>[<Set>s item]
(from s [item]))
(fn remove <Set>[<Set>s item]
(fold
(lambda [acc elem]
(if (eq? elem item)
acc
(from acc elem)))
#{}
s))
(fn union <Set>[]
(apply from %%))
(fn intersection <Set>[<Set>s1 <Set>s2]
(fold
(lambda [acc elem]
(if (has s2 elem)
(add acc elem)
acc))
#{}
s1))
(fn difference <Set>[<Set>s1 <Set>s2]
(fold
(lambda [acc elem]
(if (has s2 elem)
acc
(add acc elem)))
#{}
s1))
)
(def set? (guard set.type))
)

View file

@ -0,0 +1,76 @@
(seq
(namespace str
(def type :str)
(def new builtins.str.new)
(def __call__ new)
(fn len <Int>[<Str>s]
(vec.len (vec.from s)))
(fn concat <Str>[]
(apply new %%))
(fn empty? <Bool>[<Str>s]
(eq? s ""))
(fn is-empty? <Bool>[<Str>s]
(empty? s))
(fn chars-at <Str>[<Str>s <Int>idx]
(vec.get (vec.from s) idx))
(fn first <Str>[<Str>s]
(chars-at s 0))
(fn last <Str>[<Str>s]
(chars-at s (-- (len s))))
(fn substring <Str>[<Str>s <Int>start <Int>end]
(apply concat (vec.slice (vec.from s) start end)))
(fn substr <Str>[<Str>s <Int>start <Int>len-sub]
(substring s start (+ start len-sub)))
(fn take <Str>[<Str>s <Int>n]
(apply concat (vec.take (vec.from s) n)))
(fn drop <Str>[<Str>s <Int>n]
(apply concat (vec.drop (vec.from s) n)))
(fn reverse <Str>[<Str>s]
(apply concat (vec.reverse (vec.from s))))
(fn starts-with? <Bool>[<Str>s <Str>prefix]
(eq? (substring s 0 (len prefix)) prefix))
(fn ends-with? <Bool>[<Str>s <Str>suffix]
(eq? (substring s (- (len s) (len suffix)) (len s)) suffix))
(fn split <Vec>[<Str>s <Str>sep] (seq
(vec.fold
(lambda [acc elem _]
(if-eq (elem sep)
(vec.conj acc "")
(vec.conj (vec.take acc (- (len acc) 1))
(str.concat (vec.last acc) elem)
)
))
[""]
(vec.from s)
)))
(fn join <Str>[<Str>sep <Vec>strs]
(if (vec.empty? strs)
""
(vec.fold
(lambda [acc elem _]
(new acc sep elem))
(vec.first strs)
(vec.tail strs))))
(fn map <Str>[callback self]
(join "" (vec.map callback (vec.from self))))
)
(def str? (guard str.type))
)

View file

@ -0,0 +1,22 @@
(seq
(defmacro struct [name fields] (seq
(for field $fields
(if (bool.not (kw? %1))
(throw! "Struct fields must be keywords"))
)
(def $name {
:__struct_type__ true
:new (lambda [data] (vec.map
#(if (map.has-key? data %1)
map.get data %1
(throw! "Missing field: " %1)
)
$fields))
})
))
(fn struct-type? [v]
(if (bool.and (map? v) (map.get v :__struct_type__))
true
false))
)

301
modules/core/types/vec.owa Normal file
View file

@ -0,0 +1,301 @@
(namespace vec
(def type :vec)
(def is? (guard type))
(def new builtins.vec.new)
(def __call__ new)
(def fold builtins.vec.fold)
(def from builtins.vec.from)
;; Empty vector
(def empty [])
;; Guard to check if a value is a empty vector
(fn empty? [<Vec>v]
(eq? v empty))
;; Get the count of items in a vector
(fn len [<Vec>v]
(fold (lambda [acc _] (+ acc 1)) 0 v))
;; Append an item to the end of a vector
(fn conj [<Vec>v item]
(apply new v [item]))
;; Check if a vector contains an item
(fn has [<Vec>v item]
(fold
(lambda [acc elem _]
(bool.or acc (eq? elem item)))
false
v))
;; Get an item from a vector by index
(fn get [<Vec>v idx]
(fold
(lambda [result elem index]
(if (eq? index idx)
elem
result))
null
v))
;; Update an item in a vector
;; If the index is out of bounds, the original vector is returned
(fn assoc [<Vec>v <Int>index item]
(fold
(lambda [acc elem idx]
(if (eq? idx index)
(conj acc item)
(conj acc elem)))
[]
v))
;; Remove an item from a vector by index
;; If the index is out of bounds, the original vector is returned
(fn remove [<Vec>v <Int>index]
(filter
(lambda [elem idx]
(nq? idx index))
v))
;; Reverse a vector
(fn reverse [<Vec>v]
(fold
(lambda [acc elem _]
(apply merge elem acc))
[]
v))
;; Flatten a vector of vectors into a single vector
(fn flat [<Vec>v]
(fold
(lambda [acc elem _]
(apply conj [acc elem]))
[]
v))
;; Get the first item in a vector
(fn first [<Vec>v]
(get v 0))
;; Get the last item in a vector
(fn last [<Vec>v]
(get v (-- (len v))))
;; Get all items except the first
(fn tail [<Vec>v]
(filter
(lambda [elem idx]
(nq? idx 0))
v))
;; Get the first n items from a vector
(fn take [<Vec>v <Int>n]
(filter
(lambda [elem idx]
(lt? idx n))
v))
;; Get all items except the first n
(fn drop [<Vec>v n]
(filter
(lambda [elem idx]
(gte? idx n))
v))
;; Get all items except the last
(fn drop-last [<Vec>v]
(filter
(lambda [elem idx]
(nq? idx (-- (len v))))
v))
(fn enumerate [<Vec>v]
(map
(lambda [elem idx]
(new [idx elem]))
v))
(fn zip [v1 v2]
(map
(lambda [elem idx]
(new elem (get v2 idx)))
v1))
(fn slice [<Vec>v start end]
(filter
(lambda [elem idx]
(bool.and (gte? idx start) (lt? idx end)))
v))
(fn includes? [<Vec>v item]
(has v item))
(fn index-of [<Vec>v item]
(fold
(lambda [result elem idx]
(if (null? result)
(if (eq? elem item)
idx
result)
result))
null
v))
(fn join-str [sep v]
(if (empty? v)
""
(fold
(lambda [acc elem _]
(str.new acc sep elem))
(first v)
(tail v))))
;; Map a callback to each item in a vector
(fn map [callback <Vec>v]
(fold
(lambda [acc elem index]
(apply conj [acc (callback elem index)]))
[]
v))
;; Filter a vector by a callback
(fn filter [callback <Vec>v]
(fold
(lambda [acc elem index]
(if (callback elem index)
(apply conj [acc elem])
acc))
[]
v))
;; Find the first item that matches the callback
(fn find [callback <Vec>v]
(fold
(lambda [result elem index]
(if (null? result)
(if (callback elem index)
elem
result)
result))
null
v))
;; Check if every item in a vector matches the callback
;; TODO: Break at first non-match for better performance
(fn every? <Bool>[callback <Vec>v]
(fold
(lambda [result elem idx]
(bool.and result (callback elem idx)))
true
v))
;; Check if any item in a vector matches the callback
;; TODO: Break at first match for better performance
(fn some? <Bool>[callback <Vec>v]
(fold
(lambda [result elem idx]
(bool.or result (callback elem idx)))
false
v))
;; Reduce a vector into a single value
(fn reduce <Bool>[callback initial <Vec>v]
(fold callback initial v))
;; Count the number of items that match the callback
(fn count [callback v]
(fold
(lambda [count elem idx]
(if (callback elem idx)
(+ count 1)
count))
0
v))
;; Merge all vectors into a single vector
(fn merge []
(fold
(lambda [acc elem _]
(apply conj [acc elem]))
[]
%%))
(fn max-by [comparator v]
(fold
(lambda [current elem _]
(if (null? current)
elem
(if (eq? (comparator elem current) :greater)
elem
current)))
null
v))
(fn min-by [comparator v]
(fold
(lambda [current elem _]
(if (null? current)
elem
(if (eq? (comparator elem current) :less)
elem
current)))
null
v))
(fn uniq [<Vec>v]
(fold
(lambda [result elem _]
(if (has result elem)
result
(conj result elem)))
[]
v))
(fn partition [callback v]
(fold
(lambda [result elem idx]
(seq
(def group (if (callback elem idx) 0 1))
(def current-part (get result group))
(assoc result group (conj current-part elem))))
[[] []]
v))
(fn group-by [key-fn v]
(fold
(lambda [result elem idx]
(seq
(def key (key-fn elem idx))
(def group (map.get result key))
(def new-group (if (null? group) [] group))
(map.put result key (conj new-group elem))))
(map.new)
v))
(fn flatten [<Vec>v]
(fold
(lambda [result elem _]
(if (vec.is? elem)
(conj result (flatten elem))
(conj result elem)))
[]
v))
(fn distinct [<Vec>v]
(uniq v))
(fn compact [<Vec>v]
(filter
(lambda [elem _]
(not (null? elem)))
v))
(fn sum [<Vec>v]
(fold
(lambda [acc elem _]
(+ acc elem))
0
v))
)

19
modules/owu/cli/main.owa Normal file
View file

@ -0,0 +1,19 @@
(seq
(def cmd (vec.first argv))
(def args (vec.tail argv))
(if (bool.or (null? cmd) (str.starts-with? cmd "-")) (seq
(trace "Error: No command specified")
(return 1)
))
(try
(match cmd
("run" (include "run"))
(_ (trace "Error: Command '" cmd "' not found"))
)
(lambda [msg type] (seq
(trace type ": " msg)
(return 1)))
)
)

43
modules/owu/cli/run.owa Normal file
View file

@ -0,0 +1,43 @@
(seq
(def target (vec.find #(bool.not (str.starts-with? %1 "-")) (vec.reverse args)))
(if (null? target) (seq
(trace "No target specified")
(return)
))
(fn parse-arg [args name] (seq
(def idx (vec.index-of args name))
(if (null? idx)
null
(vec.get args (+ idx 1))
)
))
(def no-builtins? (vec.has args "--no-builtins"))
(def no-core? (vec.has args "--no-core"))
(def no-std? (vec.has args "--no-std"))
(def is-debug? (vec.has args "--debug"))
(def test-target (parse-arg args "--test"))
(exec
{
:__runtime__ :owu
:__dir__ ""
:__main__ target
:__test__ test-target
}
(seq
(unless no-builtins?
(def builtins builtins))
(unless (bool.or no-builtins? no-core?)
(include (str.concat modules-dir "/core")))
(unless (bool.or no-builtins? no-core? no-std?)
(include (str.concat modules-dir "/std")))
(def __is_debug__ is-debug?)
)
target
)
)

15
modules/owu/main.owa Normal file
View file

@ -0,0 +1,15 @@
(builtins.seq
(builtins.include "../core")
(builtins.include "../std")
(namespace owu
(def argv (platform.args))
(def modules-dir (str.concat __dir__ "/.."))
(if (eq? (vec.len argv) 0) (seq
(return 1)
))
(include "cli")
(return 0)
)
)

6
modules/std/main.owa Normal file
View file

@ -0,0 +1,6 @@
(seq
(include "platform")
(include "path")
(test.space "std" (include "tests/main"))
)

222
modules/std/path.owa Normal file
View file

@ -0,0 +1,222 @@
(namespace path
;; Platform-specific path separator.
(def sep (match platform.os-family
(:windows "\\")
(_ "/")
))
;; Internal: converts "/" to "\" on Windows; no-op on other platforms.
(fn norm-seps <Str>[<Str>p]
(match platform.os-family
(:windows (str.map (lambda [c _] (if (eq? c "/") "\\" c)) p))
(_ p)))
;; Returns the root prefix of a path.
;;
;; Unix — "/" for paths starting with "/", "" for relative.
;; Windows — "\" for UNC/rooted paths ("\" or "/" prefix)
;; "X:\" for drive-letter absolute paths ("X:\" or "X:/")
;; "" for all relative paths ("X:path" is relative on Windows)
(fn root <Str>[<Str>p]
(match platform.os-family
(:windows
(if (bool.or (str.starts-with? p "\\") (str.starts-with? p "/"))
"\\"
(if (bool.and (gte? (str.len p) 3)
(bool.and (eq? (str.chars-at p 1) ":")
(bool.or (eq? (str.chars-at p 2) "\\")
(eq? (str.chars-at p 2) "/"))))
(str.concat (str.take p 2) "\\")
"")))
(_ (if (str.starts-with? p "/") "/" ""))
))
;; Returns true if the path is absolute (has a non-empty root prefix).
(fn absolute? <Bool>[<Str>p]
(bool.not (str.empty? (root p))))
;; Returns true if the path is relative (has no root prefix).
(fn relative? <Bool>[<Str>p]
(bool.not (absolute? p)))
;; Splits a path into its string components using the platform separator.
;; On Windows "/" is also accepted as a separator.
;; The root prefix (if any) is preserved as the first component so that
;; split + join is a round-trip operation.
(fn split <Vec>[<Str>p]
(if (str.empty? p)
[]
(str.split (norm-seps p) sep)))
;; Joins path components with the platform separator.
(fn join <Str>[<Vec>p]
(str.join sep p))
;; Removes "." components, resolves ".." components, and returns the
;; canonical form of the path using the platform separator.
;;
;; Bug fixes vs. previous version:
;; • absolute paths that collapse entirely return the root (e.g. "/" not "")
;; • Windows drive-letter paths no longer get a spurious leading "\"
(fn normalize <Str>[<Str>p]
(seq
(def r (root p))
;; Work only on the non-root body so that root components never
;; participate in ".." resolution.
(def body (norm-seps (str.drop p (str.len r))))
(def parts (if (str.empty? body) [] (str.split body sep)))
(def normalized [])
(for part parts
(if (bool.or (eq? part ".") (str.empty? part)) (continue))
(set! normalized (if (eq? part "..")
(vec.drop-last normalized)
(vec.conj normalized part))))
(def result (join normalized))
(if (str.empty? r)
result
(if (str.empty? result)
r
(str.concat r result)))))
;; Returns the parent directory of the path.
;; • Paths directly under the root return the root string.
;; • Top-level relative paths (e.g. "a") return "".
;;
;; Bug fix vs. previous version: returns sep-based root, not hardcoded "/".
(fn parent <Str>[<Str>p] (seq
(def norm (normalize p))
(def r (root norm))
(def body (str.drop norm (str.len r)))
(def parts (if (str.empty? body)
[]
(vec.filter
(lambda [part _] (bool.not (str.empty? part)))
(str.split body sep))))
(def parent-parts (vec.drop-last parts))
(if (vec.empty? parent-parts)
r
(str.concat r (join parent-parts)))))
;; Returns the last component of the path (filename or final directory name).
;; Returns "" for paths that end with a separator.
(fn basename <Str>[<Str>p] (seq
(def parts (split p))
(if (vec.empty? parts) "" (vec.last parts))))
;; Returns the file extension including the leading dot, or "" if none.
;;
;; Rules:
;; • Hidden files whose name starts with "." have no extension.
;; • A trailing dot (e.g. "file.") is not considered an extension.
;; • Only the last dot in the basename is used (e.g. "a.tar.gz" → ".gz").
;;
;; Examples:
;; "file.txt" → ".txt"
;; "archive.tar.gz" → ".gz"
;; "file" → ""
;; ".hidden" → ""
;; "file." → ""
(fn ext <Str>[<Str>p] (seq
(def base (basename p))
(def dot-idx (vec.fold
(lambda [acc elem idx]
(if (eq? elem ".") idx acc))
-1
(vec.from base)))
(if (bool.or (lte? dot-idx 0) (eq? dot-idx (-- (str.len base))))
""
(str.drop base dot-idx))))
;; Returns the filename without its extension.
;;
;; Examples:
;; "file.txt" → "file"
;; "archive.tar.gz" → "archive.tar"
;; "file" → "file"
;; ".hidden" → ".hidden"
(fn stem <Str>[<Str>p] (seq
(def base (basename p))
(def e (ext p))
(if (str.empty? e)
base
(str.take base (- (str.len base) (str.len e))))))
;; Returns the path with its extension replaced by new-ext.
;; Pass "" as new-ext to strip the extension entirely.
;; The directory prefix and stem are preserved unchanged.
;;
;; Examples:
;; (with-ext "file.txt" ".md") → "file.md"
;; (with-ext "file.txt" "") → "file"
;; (with-ext "file" ".txt") → "file.txt"
(fn with-ext <Str>[<Str>p <Str>new-ext] (seq
(def e (ext p))
(def without-ext
(if (str.empty? e)
p
(str.take p (- (str.len p) (str.len e)))))
(str.concat without-ext new-ext)))
;; Resolves path relative to base and returns the normalized result.
;; If path is already absolute it is returned normalized, ignoring base.
;; Otherwise base + sep + path is normalized.
;;
;; Examples (Unix):
;; (resolve "/base" "file.txt") → "/base/file.txt"
;; (resolve "/base/dir" "../file.txt") → "/base/file.txt"
;; (resolve "/other" "/abs") → "/abs"
(fn resolve <Str>[<Str>base <Str>p]
(if (absolute? p)
(normalize p)
(normalize (str.concat base sep p))))
;; Computes the relative path from base to target.
;;
;; • Returns "." when base and target are the same path.
;; • When the roots differ (e.g. different Windows drive letters)
;; the normalized target is returned unchanged.
;;
;; Examples (Unix):
;; (relative-to "/a/b" "/a/b/c/d") → "c/d"
;; (relative-to "/a/b" "/a/c") → "../c"
;; (relative-to "/a/b/c" "/a") → "../.."
;; (relative-to "/a/b" "/a/b") → "."
(fn relative-to <Str>[<Str>base <Str>target] (seq
(def norm-base (normalize base))
(def norm-target (normalize target))
(def base-r (root norm-base))
(def target-r (root norm-target))
(if (nq? base-r target-r)
norm-target
(seq
(def base-body (str.drop norm-base (str.len base-r)))
(def target-body (str.drop norm-target (str.len target-r)))
(fn parts-of [body]
(if (str.empty? body)
[]
(vec.filter
(lambda [part _] (bool.not (str.empty? part)))
(str.split body sep))))
(def base-parts (parts-of base-body))
(def target-parts (parts-of target-body))
(def base-len (vec.len base-parts))
(def target-len (vec.len target-parts))
;; Find length of the common prefix.
(def i 0)
(while (bool.and (lt? i base-len)
(bool.and (lt? i target-len)
(eq? (vec.get base-parts i)
(vec.get target-parts i))))
(set! i (++ i)))
;; One ".." per remaining component in base, then the remaining target components.
(def ups (vec.map (lambda [_e _i] "..") (vec.drop base-parts i)))
(def remaining (vec.drop target-parts i))
(def result-parts
(vec.fold
(lambda [acc elem _] (vec.conj acc elem))
ups
remaining))
(if (vec.empty? result-parts)
"."
(join result-parts))))))
)

55
modules/std/platform.owa Normal file
View file

@ -0,0 +1,55 @@
(namespace platform
(def os-name builtins.platform.os-name)
(def os-family builtins.platform.os-family)
(def arch builtins.platform.arch)
(def argv builtins.platform.argv)
(def executable #(vec.get (argv) 0))
(def args #(vec.slice (argv) 1 (vec.len (argv))))
(def debug? (lookup __is_debug__ false))
(def os-names [
:linux
:windows
:macos
:android
:ios
:openbsd
:freebsd
:netbsd
:wasi
:hermit
:aix
:apple
:dragonfly
:emscripten
:espidf
:fortanix
:uefi
:fuchsia
:haiku
:hermit
:watchos
:visionos
:tvos
:horizon
:hurd
:illumos
:l4re
:nto
:redox
:solaris
:solid_asp3
:vexos
:vita
:vxworks
:xous
])
(def os-families [
:unix
:windows
:itron
:wasm
])
)

View file

@ -0,0 +1,3 @@
(seq
(include "path")
)

286
modules/std/tests/path.owa Normal file
View file

@ -0,0 +1,286 @@
(namespace test.path
(test.case "path.sep"
(assert.eq! path.sep (match platform.os-family (:windows "\\") (_ "/")))
)
(test.case "path.root"
;; Relative paths and empty string have no root on all platforms.
(assert.eq! (path.root "relative") "")
(assert.eq! (path.root "") "")
(match platform.os-family
(:windows (seq
;; Rooted by backslash or forward-slash → canonical "\"
(assert.eq! (path.root "\\a\\b") "\\")
(assert.eq! (path.root "/a/b") "\\")
;; Drive-letter absolute ("X:\" or "X:/")
(assert.eq! (path.root "C:\\a\\b") "C:\\")
(assert.eq! (path.root "C:/a/b") "C:\\")
(assert.eq! (path.root "C:\\") "C:\\")
;; Drive letter WITHOUT a separator is relative on Windows
(assert.eq! (path.root "C:") "")
(assert.eq! (path.root "C:relative") "")
))
(_ (seq
(assert.eq! (path.root "/") "/")
(assert.eq! (path.root "/a/b") "/")
))
)
)
(test.case "path.absolute?"
;; "/" is recognised as absolute on all platforms.
(assert.ok! (path.absolute? "/absolute/unix"))
(assert.ok! (path.absolute? "/"))
(assert.not! (path.absolute? "relative/unix"))
(assert.not! (path.absolute? "./relative/unix"))
(match platform.os-family
(:windows (seq
;; Drive-letter prefix (both separators)
(assert.ok! (path.absolute? "C:\\absolute\\windows"))
(assert.ok! (path.absolute? "C:/absolute/windows"))
;; UNC / rooted backslash
(assert.ok! (path.absolute? "\\absolute\\unc"))
;; Relative Windows paths
(assert.not! (path.absolute? "relative\\windows"))
(assert.not! (path.absolute? ".\\relative\\windows"))
;; Drive letter without separator is relative
(assert.not! (path.absolute? "C:relative"))
))
(_ (void))
)
)
(test.case "path.relative?"
(assert.ok! (path.relative? "relative/unix"))
(assert.not! (path.relative? "/absolute/unix"))
(match platform.os-family
(:windows (seq
(assert.ok! (path.relative? "relative\\windows"))
(assert.not! (path.relative? "C:\\absolute\\windows"))
(assert.not! (path.relative? "\\absolute\\unc"))
(assert.ok! (path.relative? "C:relative"))
))
(_ (void))
)
)
(test.case "path.split"
(assert.eq! (path.split "") [])
(assert.eq! (path.split "a") ["a"])
(match platform.os-family
(:windows (seq
(assert.eq! (path.split "a\\b\\c") ["a" "b" "c"])
;; Windows also accepts "/" as separator
(assert.eq! (path.split "a/b/c") ["a" "b" "c"])
;; Drive-letter prefix is kept as the first component (round-trip with join)
(assert.eq! (path.split "C:\\a\\b") ["C:" "a" "b"])
(assert.eq! (path.split "C:/a/b") ["C:" "a" "b"])
))
(_ (seq
(assert.eq! (path.split "a/b/c") ["a" "b" "c"])
))
)
)
(test.case "path.join"
(assert.eq! (path.join []) "")
(assert.eq! (path.join ["a"]) "a")
(match platform.os-family
(:windows (seq
(assert.eq! (path.join ["a" "b" "c"]) "a\\b\\c")
(assert.eq! (path.join ["C:" "a" "b"]) "C:\\a\\b")
))
(_ (seq
(assert.eq! (path.join ["a" "b" "c"]) (str.concat "a" path.sep "b" path.sep "c"))
))
)
)
(test.case "path.normalize"
(match platform.os-family
(:windows (seq
;; Dot component is stripped
(assert.eq! (path.normalize "a\\.\\b") (path.join ["a" "b"]))
;; Double-dot collapses parent
(assert.eq! (path.normalize "a\\..\\b") "b")
;; Rooted-backslash absolute path
(assert.eq! (path.normalize "\\a\\..\\b") (str.concat path.sep "b"))
;; Bug fix: absolute path collapsing to root returns the root, not ""
(assert.eq! (path.normalize "\\") "\\")
(assert.eq! (path.normalize "\\a\\..") "\\")
;; Drive-letter paths: no spurious leading "\" added
(assert.eq! (path.normalize "C:\\a\\..\\b") "C:\\b")
(assert.eq! (path.normalize "C:\\") "C:\\")
(assert.eq! (path.normalize "C:\\a\\b\\..\\..") "C:\\")
;; Windows normalises "/" separators as well
(assert.eq! (path.normalize "a/./b") (path.join ["a" "b"]))
(assert.eq! (path.normalize "C:/a/../b") "C:\\b")
))
(_ (seq
;; Dot component is stripped
(assert.eq! (path.normalize "a/./b") (path.join ["a" "b"]))
;; Double-dot collapses parent
(assert.eq! (path.normalize "a/../b") "b")
;; Rooted absolute path
(assert.eq! (path.normalize "/a/../b") (str.concat path.sep "b"))
;; Bug fix: absolute path collapsing to root returns "/", not ""
(assert.eq! (path.normalize "/") "/")
(assert.eq! (path.normalize "/a/..") "/")
(assert.eq! (path.normalize "/a/b/../..") "/")
))
)
)
(test.case "path.parent"
;; Top-level relative path always returns "" on every platform.
(assert.eq! (path.parent "a") "")
(match platform.os-family
(:windows (seq
(assert.eq! (path.parent "a\\b\\c") "a\\b")
(assert.eq! (path.parent "a\\b") "a")
;; Bug fix: rooted paths return "\" not hardcoded "/"
(assert.eq! (path.parent "\\") "\\")
(assert.eq! (path.parent "\\..\\a\\.\\..\\") "\\")
;; Drive-letter paths
(assert.eq! (path.parent "C:\\") "C:\\")
(assert.eq! (path.parent "C:\\a") "C:\\")
(assert.eq! (path.parent "C:\\a\\b") "C:\\a")
(assert.eq! (path.parent "C:\\a\\b\\c") "C:\\a\\b")
))
(_ (seq
(assert.eq! (path.parent "a/b/c") "a/b")
(assert.eq! (path.parent "a/b") "a")
(assert.eq! (path.parent "/") "/")
(assert.eq! (path.parent "/../a/./../") "/")
))
)
)
(test.case "path.basename"
(assert.eq! (path.basename "file.txt") "file.txt")
(match platform.os-family
(:windows (seq
(assert.eq! (path.basename "a\\b\\c") "c")
;; Trailing separator → empty basename
(assert.eq! (path.basename "a\\b\\") "")
(assert.eq! (path.basename "C:\\Users\\file.txt") "file.txt")
;; Windows also handles "/" separator
(assert.eq! (path.basename "a/b/c") "c")
))
(_ (seq
(assert.eq! (path.basename "a/b/c") "c")
(assert.eq! (path.basename "a/b/") "")
))
)
)
(test.case "path.ext"
;; Basic cases work on all platforms (basename is a simple name)
(assert.eq! (path.ext "file.txt") ".txt")
(assert.eq! (path.ext "file") "")
(assert.eq! (path.ext ".hidden") "")
(assert.eq! (path.ext "archive.tar.gz") ".gz")
(assert.eq! (path.ext "file.") "")
(assert.eq! (path.ext "") "")
(match platform.os-family
(:windows (seq
(assert.eq! (path.ext "C:\\Users\\file.txt") ".txt")
(assert.eq! (path.ext "a\\b\\no-ext") "")
))
(_ (seq
(assert.eq! (path.ext "a/b/file.txt") ".txt")
(assert.eq! (path.ext "a/b/no-ext") "")
))
)
)
(test.case "path.stem"
(assert.eq! (path.stem "file.txt") "file")
(assert.eq! (path.stem "file") "file")
(assert.eq! (path.stem ".hidden") ".hidden")
(assert.eq! (path.stem "archive.tar.gz") "archive.tar")
(assert.eq! (path.stem "") "")
(match platform.os-family
(:windows (seq
(assert.eq! (path.stem "C:\\Users\\file.txt") "file")
))
(_ (seq
(assert.eq! (path.stem "a/b/file.txt") "file")
))
)
)
(test.case "path.with-ext"
(assert.eq! (path.with-ext "file.txt" ".md") "file.md")
(assert.eq! (path.with-ext "file.txt" "") "file")
(assert.eq! (path.with-ext "file" ".txt") "file.txt")
(assert.eq! (path.with-ext "archive.tar.gz" ".bz2") "archive.tar.bz2")
;; Hidden file: no extension → new-ext is appended
(assert.eq! (path.with-ext ".hidden" ".txt") ".hidden.txt")
;; Directory prefix is preserved
(match platform.os-family
(:windows (seq
(assert.eq! (path.with-ext "C:\\a\\file.txt" ".md") "C:\\a\\file.md")
))
(_ (seq
(assert.eq! (path.with-ext "a/b/file.txt" ".md") "a/b/file.md")
))
)
)
(test.case "path.resolve"
(match platform.os-family
(:windows (seq
;; Relative path is joined to base and normalized
(assert.eq! (path.resolve "C:\\base" "file.txt") "C:\\base\\file.txt")
;; ".." is resolved against base
(assert.eq! (path.resolve "C:\\base\\dir" "..\\file.txt") "C:\\base\\file.txt")
;; Absolute path ignores base
(assert.eq! (path.resolve "C:\\other" "C:\\absolute") "C:\\absolute")
;; Deep traversal
(assert.eq! (path.resolve "C:\\a\\b\\c" "..\\..\\d") "C:\\a\\d")
))
(_ (seq
(assert.eq! (path.resolve "/base" "file.txt") "/base/file.txt")
(assert.eq! (path.resolve "/base/dir" "../file.txt") "/base/file.txt")
;; Absolute path ignores base
(assert.eq! (path.resolve "/other" "/absolute") "/absolute")
;; Deep traversal
(assert.eq! (path.resolve "/a/b/c" "../../d") "/a/d")
;; Relative base + relative path
(assert.eq! (path.resolve "base/dir" "file.txt") "base/dir/file.txt")
))
)
)
(test.case "path.relative-to"
(match platform.os-family
(:windows (seq
;; Child path
(assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\b\\c\\d") "c\\d")
;; Sibling path
(assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\c") "..\\c")
;; Ancestor path
(assert.eq! (path.relative-to "C:\\a\\b\\c" "C:\\a") "..\\..")
;; Same path → "."
(assert.eq! (path.relative-to "C:\\a\\b" "C:\\a\\b") ".")
;; From root
(assert.eq! (path.relative-to "C:\\" "C:\\a\\b") "a\\b")
;; Different drives → return normalized target unchanged
(assert.eq! (path.relative-to "C:\\a" "D:\\b") "D:\\b")
))
(_ (seq
(assert.eq! (path.relative-to "/a/b" "/a/b/c/d") "c/d")
(assert.eq! (path.relative-to "/a/b" "/a/c") "../c")
(assert.eq! (path.relative-to "/a/b/c" "/a") "../..")
;; Same path → "."
(assert.eq! (path.relative-to "/a/b" "/a/b") ".")
;; From root
(assert.eq! (path.relative-to "/" "/a/b") "a/b")
;; Relative paths
(assert.eq! (path.relative-to "a/b" "a/b/c") "c")
(assert.eq! (path.relative-to "a/b" "a/c") "../c")
))
)
)
)