diff --git a/boot/cimport.fch b/boot/cimport.fch index 84f8e9d..5699f78 100644 --- a/boot/cimport.fch +++ b/boot/cimport.fch @@ -193,7 +193,7 @@ fn visitField(CXCursor field, ptr data) -> i32 { } else if ft.kind == CXType_Record && clang_getCursorKind(clang_getTypeDeclaration(ft)) == CXCursor_UnionDecl { f.hidden = true // unions are kept as opaque bytes } else { - m := mapType(c.imp, ft, false) + m := mapType(c.imp, ft, false, false) if m.ok { f.ty = m.ty } else { @@ -242,7 +242,9 @@ fn sized(int size, bool signed) -> Mapped { // C type -> Finch type. Not ok (with a reason) if Finch can't express it. // `signature`: a char * is a str (a function's parameters and result); anywhere else it stays a ptr[char]. -fn mapType(ptr[Importer] imp, CXType type, bool signature) -> Mapped { +// A parameter that is a plain `char *` (not const) is a buffer C writes into, and stays a ptr[char]; +// only `const char *` is text going in. A result that is a char * is always text coming out. +fn mapType(ptr[Importer] imp, CXType type, bool signature, bool param) -> Mapped { t := clang_getCanonicalType(type) size := int(clang_Type_getSizeOf(t)) k := t.kind @@ -273,12 +275,14 @@ fn mapType(ptr[Importer] imp, CXType type, bool signature) -> Mapped { if k == CXType_Pointer { pointee := clang_getCanonicalType(clang_getPointeeType(t)) if (pointee.kind == CXType_Char_S || pointee.kind == CXType_Char_U) && signature { - return ok(ast.simple("str")) // char* in a signature is a C string + if !param || clang_isConstQualifiedType(pointee) != 0 { + return ok(ast.simple("str")) // a C string + } } if pointee.kind == CXType_Void || pointee.kind == CXType_FunctionProto || pointee.kind == CXType_FunctionNoProto { return ok(ast.simple("ptr")) // an opaque `ptr` you can pass around } - inner := mapType(imp, pointee, false) + inner := mapType(imp, pointee, false, false) if !inner.ok || inner.ty.kind == "void" || inner.ty.kind == "fixed" { return ok(ast.simple("ptr")) } @@ -295,7 +299,7 @@ fn mapType(ptr[Importer] imp, CXType type, bool signature) -> Mapped { return ok(ast.Type(kind: "named", name: name, module: "C")) } if k == CXType_ConstantArray { - inner := mapType(imp, clang_getArrayElementType(t), false) + inner := mapType(imp, clang_getArrayElementType(t), false, false) if !inner.ok { return inner } @@ -447,7 +451,7 @@ fn visit(ptr[Importer] imp, CXCursor c) { imp.out.fns[name] = f return } - r := mapType(imp, clang_getResultType(ft), true) + r := mapType(imp, clang_getResultType(ft), true, false) if r.ok { f.ret = r.ty } else { @@ -458,7 +462,7 @@ fn visit(ptr[Importer] imp, CXCursor c) { if f.unsupported.len > 0 { break } - p := mapType(imp, clang_getArgType(ft, u32(k)), true) + p := mapType(imp, clang_getArgType(ft, u32(k)), true, true) if p.ok { f.params.push(p.ty) } else { @@ -471,7 +475,7 @@ fn visit(ptr[Importer] imp, CXCursor c) { if clang_Cursor_getStorageClass(c) != CX_SC_Extern || imp.out.globals.has(name) { return } - g := mapType(imp, clang_getCursorType(c), false) + g := mapType(imp, clang_getCursorType(c), false, false) if g.ok { imp.out.globals[name] = g.ty } diff --git a/boot/gen.fch b/boot/gen.fch index 8aae16c..af70c7f 100644 --- a/boot/gen.fch +++ b/boot/gen.fch @@ -3729,6 +3729,14 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { } else if k == "bool" { z := op(g, "zext i1 " + v.v + " to i32") emit(g, "call void @finch_str_from_bool(ptr " + out + ", i32 " + z + ")") + } else if k == "ptr" { + // a C string, copied + store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) + emit(g, "call void @finch_str_from_c(ptr " + out + ", ptr " + v.v + ")") + } else if k == "array" && (v.ty.elem[0].kind == "u8" || v.ty.elem[0].kind == "char") { + // bytes become text + store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) + emit(g, "call void @finch_str_from_bytes(ptr " + out + ", ptr " + spill(g, v) + ")") } else if k == "bool" || k == "char" || k == "array" || k == "map" || k == "struct" || k == "enum" { store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) emitFormat(g, out, v.v, v.ty, false) @@ -4430,6 +4438,7 @@ fn runtimeDecls() -> str { "declare i32 @printf(ptr, ...)", "declare i32 @fflush(ptr)", "declare void @finch_str_from_c(ptr, ptr)", + "declare void @finch_str_from_bytes(ptr, ptr)", "declare void @exit(i32) noreturn", "declare void @free(ptr)", ] diff --git a/boot/main.fch b/boot/main.fch index 2681266..599fdc3 100644 --- a/boot/main.fch +++ b/boot/main.fch @@ -38,6 +38,21 @@ struct Loader { []ast.Program progs []str files []str loaded + str std // the standard library's folder, or "" +} + +// import json: the file next to the one that imports it, else the standard library's. +// A program's own json.fch wins over the library's. +fn findModule(ptr[Loader] l, str from, str name) -> str { + local := dirOf(from) + "/" + name + ".fch" + if file_exists(local) || l.std.len == 0 { + return local + } + shared := l.std + "/" + name + ".fch" + if file_exists(shared) { + return shared + } + return local } fn load(ptr[Loader] l, str path, str module) { @@ -60,7 +75,7 @@ fn load(ptr[Loader] l, str path, str module) { for name in imports { if !l.loaded.contains(name) { l.loaded.push(name) - load(l, dirOf(path) + "/" + name + ".fch", name) + load(l, findModule(l, path, name), name) } } } @@ -149,6 +164,11 @@ fn main([]str args) -> int { } loader := Loader() + for cand in [dirOf(args[0]) + "/../std", dirOf(args[0]) + "/std", "std"] { + if loader.std.len == 0 && file_exists(cand + "/path.fch") { + loader.std = cand + } + } load(addr(loader), file, "") // C headers, from every file; their folders are searched first for "local.h" diff --git a/std/json.fch b/std/json.fch new file mode 100644 index 0000000..1a85779 --- /dev/null +++ b/std/json.fch @@ -0,0 +1,518 @@ +// JSON: read it, build it, write it. +// +// import json +// +// doc := try json.parse("{\"name\": \"Finch\", \"tags\": [\"fast\", \"small\"], \"stars\": 5}") +// print(doc.get("name").str_or("")) Finch +// print(doc.get("tags").at(1).str_or("")) small +// print(doc.get("stars").int_or(0)) 5 +// print(doc.get("missing").is_null()) true (a missing key is null, so chains never fail) +// +// out := json.new_object() +// out.set("ok", json.new_bool(true)) +// list := json.new_list() +// list.push(json.new_number(1.5)) +// out.set("values", list) +// print(json.stringify(out)) {"ok":true,"values":[1.5]} +// print(json.pretty(out)) the same, over several lines + +enum Kind { Null, Bool, Number, Text, List, Object } + +// One JSON value. `kind` says which of the other fields it uses: flag for Bool, num for Number, +// text for Text, items for List, and items with keys (the same length, in the same order) for Object. +struct Json { + Kind kind + bool flag + float num + str text + []Json items + []str keys +} + +fn new_null() -> Json { + return Json(kind: Kind.Null) +} + +fn new_bool(bool b) -> Json { + return Json(kind: Kind.Bool, flag: b) +} + +fn new_number(float n) -> Json { + return Json(kind: Kind.Number, num: n) +} + +fn new_text(str s) -> Json { + return Json(kind: Kind.Text, text: s) +} + +fn new_list() -> Json { + return Json(kind: Kind.List) +} + +fn new_object() -> Json { + return Json(kind: Kind.Object) +} + +// ---------- looking inside ---------- + +fn Json.is_null() -> bool { + return self.kind == Kind.Null +} + +// the number of elements of a list, of keys of an object, of bytes of a text; 0 for the rest +fn Json.len() -> int { + return match self.kind { + List, Object -> self.items.len + Text -> self.text.len + else -> 0 + } +} + +fn Json.has(str key) -> bool { + return self.kind == Kind.Object && self.keys.contains(key) +} + +// the value of a key of an object; null if there is none (or this is not an object) +fn Json.get(str key) -> Json { + if self.kind != Kind.Object { + return new_null() + } + i := self.keys.find(key) + if i < 0 { + return new_null() + } + return self.items[i] +} + +// the element of a list; null if there is none +fn Json.at(int i) -> Json { + if self.kind != Kind.List || i < 0 || i >= self.items.len { + return new_null() + } + return self.items[i] +} + +// the value if it is of that kind, else what was given +fn Json.str_or(str fallback) -> str { + if self.kind == Kind.Text { + return self.text + } + return fallback +} + +fn Json.float_or(float fallback) -> float { + if self.kind == Kind.Number { + return self.num + } + return fallback +} + +fn Json.int_or(int fallback) -> int { + if self.kind == Kind.Number { + return int(self.num) + } + return fallback +} + +fn Json.bool_or(bool fallback) -> bool { + if self.kind == Kind.Bool { + return self.flag + } + return fallback +} + +// the keys of an object, in the order they were written +fn Json.names() -> []str { + return self.keys +} + +// ---------- building ---------- + +// add to the end of a list +fn Json.push(Json v) { + self.items.push(v) +} + +// give a key a value in an object; a key that is already there is replaced +fn Json.set(str key, Json v) { + i := self.keys.find(key) + if i >= 0 { + self.items[i] = v + return + } + self.keys.push(key) + self.items.push(v) +} + +// ---------- writing ---------- + +fn quote(str s) -> str { + []str parts = ["\""] + for c in s { + n := int(c) + if c == '"' { + parts.push("\\\"") + } else if c == '\\' { + parts.push("\\\\") + } else if c == '\n' { + parts.push("\\n") + } else if c == '\r' { + parts.push("\\r") + } else if c == '\t' { + parts.push("\\t") + } else if n < 32 { + parts.push("\\u00" + hexdigit(n / 16) + hexdigit(n % 16)) + } else { + parts.push(str(c)) + } + } + parts.push("\"") + return parts.join("") +} + +fn hexdigit(int n) -> str { + digits := "0123456789abcdef" + return str(digits[n]) +} + +fn number_text(float n) -> str { + if n == float(int(n)) && n > -1000000000000000.0 && n < 1000000000000000.0 { + return str(int(n)) + } + return str(n) +} + +struct Writer { + []str out +} + +fn write(Json j, str indent, str pad, ptr[Writer] w) { + nl := "" + inner := "" + sep := "," + colon := ":" + if indent.len > 0 { + nl = "\n" + inner = pad + indent + colon = ": " + } + match j.kind { + Null -> w.out.push("null") + Bool -> { + if j.flag { + w.out.push("true") + } else { + w.out.push("false") + } + } + Number -> w.out.push(number_text(j.num)) + Text -> w.out.push(quote(j.text)) + List -> { + if j.items.len == 0 { + w.out.push("[]") + return + } + w.out.push("[" + nl) + for i, item in j.items { + if i > 0 { + w.out.push(sep + nl) + } + w.out.push(inner) + write(item, indent, inner, w) + } + w.out.push(nl + pad + "]") + } + Object -> { + if j.items.len == 0 { + w.out.push("{}") + return + } + w.out.push("{" + nl) + for i, item in j.items { + if i > 0 { + w.out.push(sep + nl) + } + w.out.push(inner + quote(j.keys[i]) + colon) + write(item, indent, inner, w) + } + w.out.push(nl + pad + "}") + } + } +} + +// the value as text on one line +fn stringify(Json j) -> str { + w := Writer() + write(j, "", "", addr(w)) + return w.out.join("") +} + +// the value as text over several lines, indented by two spaces +fn pretty(Json j) -> str { + w := Writer() + write(j, " ", "", addr(w)) + return w.out.join("") +} + +// ---------- reading ---------- + +struct Reader { + str src + int i +} + +// the line and column of a position, for a message +fn where(ptr[Reader] r) -> str { + line := 1 + col := 1 + for k in 0..r.i { + if k < r.src.len && r.src[k] == '\n' { + line += 1 + col = 1 + } else { + col += 1 + } + } + return "line " + str(line) + ", column " + str(col) +} + +fn peek(ptr[Reader] r) -> char { + if r.i < r.src.len { + return r.src[r.i] + } + return char(0) +} + +fn skip_space(ptr[Reader] r) { + while r.i < r.src.len && (r.src[r.i] == ' ' || r.src[r.i] == '\n' || r.src[r.i] == '\t' || r.src[r.i] == '\r') { + r.i += 1 + } +} + +fn unexpected(ptr[Reader] r, str wanted) -> str { + if r.i >= r.src.len { + return "json: the text ends where " + wanted + " should be (" + where(r) + ")" + } + return "json: found '" + str(r.src[r.i]) + "' where " + wanted + " should be (" + where(r) + ")" +} + +// does `word` come next? +fn comes(ptr[Reader] r, str word) -> bool { + return r.src.len - r.i >= word.len && r.src.sub(r.i, r.i + word.len) == word +} + +fn expect_word(ptr[Reader] r, str word) -> ! { + if !comes(r, word) { + return error(unexpected(r, "'" + word + "'")) + } + r.i += word.len +} + +fn hex_value(char c) -> int { + if c >= '0' && c <= '9' { + return int(c) - int('0') + } + if c >= 'a' && c <= 'f' { + return int(c) - int('a') + 10 + } + if c >= 'A' && c <= 'F' { + return int(c) - int('A') + 10 + } + return -1 +} + +// a code point as UTF-8 +fn utf8(int cp) -> str { + []u8 b + if cp < 128 { + b.push(u8(cp)) + } else if cp < 2048 { + b.push(u8(192 + cp / 64)) + b.push(u8(128 + cp % 64)) + } else if cp < 65536 { + b.push(u8(224 + cp / 4096)) + b.push(u8(128 + (cp / 64) % 64)) + b.push(u8(128 + cp % 64)) + } else { + b.push(u8(240 + cp / 262144)) + b.push(u8(128 + (cp / 4096) % 64)) + b.push(u8(128 + (cp / 64) % 64)) + b.push(u8(128 + cp % 64)) + } + return str(b) +} + +fn read_hex4(ptr[Reader] r) -> int! { + n := 0 + for k in 0..4 { + if r.i >= r.src.len || hex_value(r.src[r.i]) < 0 { + return error(unexpected(r, "a hexadecimal digit")) + } + n = n * 16 + hex_value(r.src[r.i]) + r.i += 1 + } + return n +} + +fn read_string(ptr[Reader] r) -> str! { + r.i += 1 // the opening quote + []str parts + start := r.i + while true { + if r.i >= r.src.len { + return error("json: a text is never closed (" + where(r) + ")") + } + c := r.src[r.i] + if c == '"' { + parts.push(r.src.sub(start, r.i)) + r.i += 1 + return parts.join("") + } + if int(c) < 32 { + return error("json: a line break or control character inside a text (" + where(r) + ")") + } + if c != '\\' { + r.i += 1 + continue + } + parts.push(r.src.sub(start, r.i)) + r.i += 1 + e := peek(r) + r.i += 1 + if e == 'n' { + parts.push("\n") + } else if e == 't' { + parts.push("\t") + } else if e == 'r' { + parts.push("\r") + } else if e == 'b' { + parts.push(utf8(8)) + } else if e == 'f' { + parts.push(utf8(12)) + } else if e == '"' || e == '\\' || e == '/' { + parts.push(str(e)) + } else if e == 'u' { + cp := try read_hex4(r) + // a pair of 😀 stands for one character above U+FFFF + if cp >= 55296 && cp < 56320 && comes(r, "\\u") { + r.i += 2 + low := try read_hex4(r) + cp = 65536 + (cp - 55296) * 1024 + (low - 56320) + } + parts.push(utf8(cp)) + } else { + return error("json: unknown escape \\" + str(e) + " in a text (" + where(r) + ")") + } + start = r.i + } +} + +fn read_number(ptr[Reader] r) -> Json! { + start := r.i + if peek(r) == '-' { + r.i += 1 + } + while r.i < r.src.len && ((r.src[r.i] >= '0' && r.src[r.i] <= '9') || r.src[r.i] == '.' || r.src[r.i] == 'e' || r.src[r.i] == 'E' || r.src[r.i] == '+' || r.src[r.i] == '-') { + r.i += 1 + } + n := float(r.src.sub(start, r.i)) or { + r.i = start + return error(unexpected(r, "a number")) + } + return new_number(n) +} + +fn read_value(ptr[Reader] r) -> Json! { + skip_space(r) + c := peek(r) + if r.i >= r.src.len { + return error(unexpected(r, "a value")) + } + if c == '{' { + obj := new_object() + r.i += 1 + skip_space(r) + if peek(r) == '}' { + r.i += 1 + return obj + } + while true { + skip_space(r) + if peek(r) != '"' { + return error(unexpected(r, "a key in quotes")) + } + key := try read_string(r) + skip_space(r) + if peek(r) != ':' { + return error(unexpected(r, "':'")) + } + r.i += 1 + v := try read_value(r) + obj.set(key, v) + skip_space(r) + if peek(r) == ',' { + r.i += 1 + continue + } + if peek(r) == '}' { + r.i += 1 + return obj + } + return error(unexpected(r, "',' or '}'")) + } + } + if c == '[' { + list := new_list() + r.i += 1 + skip_space(r) + if peek(r) == ']' { + r.i += 1 + return list + } + while true { + v := try read_value(r) + list.push(v) + skip_space(r) + if peek(r) == ',' { + r.i += 1 + continue + } + if peek(r) == ']' { + r.i += 1 + return list + } + return error(unexpected(r, "',' or ']'")) + } + } + if c == '"' { + s := try read_string(r) + return new_text(s) + } + if c == 't' { + try expect_word(r, "true") + return new_bool(true) + } + if c == 'f' { + try expect_word(r, "false") + return new_bool(false) + } + if c == 'n' { + try expect_word(r, "null") + return new_null() + } + if c == '-' || (c >= '0' && c <= '9') { + return try read_number(r) + } + return error(unexpected(r, "a value")) +} + +// the JSON in `text`; an error with the line and column where it went wrong +fn parse(str text) -> Json! { + r := Reader(src: text) + v := try read_value(addr(r)) + skip_space(addr(r)) + if r.i < r.src.len { + return error(unexpected(addr(r), "the end of the text")) + } + return v +} diff --git a/std/path.fch b/std/path.fch new file mode 100644 index 0000000..c424255 --- /dev/null +++ b/std/path.fch @@ -0,0 +1,128 @@ +// Paths, as text. Elements are separated by "/". +// +// import path +// path.join("a", "b.txt") a/b.txt +// path.base("/tmp/x/a.tar.gz") a.tar.gz path.stem(...) a.tar path.ext(...) .gz +// path.dir("/tmp/x/a.txt") /tmp/x +// path.clean("a//b/../c/./d") a/c/d + +// a and b with one separator between; an absolute b replaces a +fn join(str a, str b) -> str { + if a.len == 0 { + return b + } + if b.len == 0 { + return a + } + if b[0] == '/' { + return b + } + if a[a.len - 1] == '/' { + return a + b + } + return a + "/" + b +} + +fn is_abs(str p) -> bool { + return p.len > 0 && p[0] == '/' +} + +// the last element: "a/b.txt" -> "b.txt", "a/b/" -> "b", "" -> ".", "/" -> "/" +fn base(str p) -> str { + if p.len == 0 { + return "." + } + end := p.len + while end > 1 && p[end - 1] == '/' { + end -= 1 + } + if end == 1 && p[0] == '/' { + return "/" + } + start := end + while start > 0 && p[start - 1] != '/' { + start -= 1 + } + return p.sub(start, end) +} + +// everything before the last element: "a/b.txt" -> "a", "b.txt" -> ".", "/b" -> "/" +fn dir(str p) -> str { + end := p.len + while end > 1 && p[end - 1] == '/' { + end -= 1 + } + i := end + while i > 0 && p[i - 1] != '/' { + i -= 1 + } + if i == 0 { + return "." + } + while i > 1 && p[i - 1] == '/' { + i -= 1 + } + return p.sub(0, i) +} + +// the part of the last element from its last dot: "a.tar.gz" -> ".gz"; "" when there is none +fn ext(str p) -> str { + b := base(p) + i := b.len + while i > 0 && b[i - 1] != '.' { + i -= 1 + } + if i <= 1 { // no dot, or a name that only starts with one (.bashrc) + return "" + } + return b.sub(i - 1, b.len) +} + +// the last element without its extension +fn stem(str p) -> str { + b := base(p) + e := ext(p) + return b.sub(0, b.len - e.len) +} + +// the elements of a path: "/a/b/c" -> ["a", "b", "c"] +fn parts(str p) -> []str { + []str out + for piece in p.split("/") { + if piece.len > 0 { + out.push(piece) + } + } + return out +} + +// the shortest path with the same meaning: no "//", no ".", and ".." resolved where it can be +fn clean(str p) -> str { + if p.len == 0 { + return "." + } + abs := is_abs(p) + []str out + for piece in parts(p) { + if piece == "." { + continue + } + if piece == ".." { + if out.len > 0 && out[out.len - 1] != ".." { + out.pop() + } else if !abs { + out.push("..") + } + continue + } + out.push(piece) + } + joined := out.join("/") + if abs { + return "/" + joined + } + if joined.len == 0 { + return "." + } + return joined +} diff --git a/std/proc.fch b/std/proc.fch new file mode 100644 index 0000000..1837c57 --- /dev/null +++ b/std/proc.fch @@ -0,0 +1,48 @@ +// Programs: run a command, read what it printed, look at the environment. +// +// import proc +// code := proc.run("make") the exit code; what the command prints goes to the screen +// text := try proc.output("git describe") what the command printed; an error if it failed +// proc.env("HOME", "/") an environment variable, or the second value if it is not set +import "stdlib.h" +import "unistd.h" +import "time.h" + +// run a command; its output goes to the screen. Gives its exit code. +fn run(str command) -> int { + return shell(command) +} + +// run a command and give what it printed. Fails, with its output as the message, if it exits with an error. +fn output(str command) -> str! { + tmp := "/tmp/finch_out_" + str(getpid()) + "_" + str(int(time(null))) + code := shell("(" + command + ") > '" + tmp + "' 2>&1") + text := read_file(tmp) or "" + delete_file(tmp) + if code != 0 { + return error("'" + command + "' exited with code " + str(code) + ": " + text.trim()) + } + return text +} + +fn env(str name, str fallback) -> str { + v := getenv(name) + if v.len == 0 { + return fallback + } + return v +} + +fn pid() -> int { + return int(getpid()) +} + +// the folder the program is running in +fn cwd() -> str { + []char buf + for i in 0..4096 { + buf.push(char(0)) + } + getcwd(buf.ptr, 4096) + return str(buf.ptr) +} diff --git a/std/time.fch b/std/time.fch new file mode 100644 index 0000000..1bdcc17 --- /dev/null +++ b/std/time.fch @@ -0,0 +1,88 @@ +// Time: the clock, sleeping, and dates. +// +// import time +// start := time.millis() milliseconds from a clock that only goes forward (to measure with) +// time.sleep(250) wait a quarter of a second +// time.now() seconds since 1970-01-01 00:00 UTC +// d := time.date(time.now()) the parts: d.year, d.month, d.day, d.hour, d.minute, d.second +// time.format(time.now(), "%Y-%m-%d %H:%M") as text, in the computer's time zone +import "time.h" + +struct Date { + int year + int month // 1 to 12 + int day // 1 to 31 + int hour + int minute + int second + int weekday // 0 is Sunday + int yearday // 1 to 366 +} + +// seconds since 1970-01-01 00:00 UTC +fn now() -> int { + return int(time(null)) +} + +// milliseconds on a clock that only goes forward: for measuring how long something took +fn millis() -> int { + timespec ts + clock_gettime(CLOCK_MONOTONIC, addr(ts)) + return int(ts.tv_sec) * 1000 + int(ts.tv_nsec) / 1000000 +} + +fn sleep(int ms) { + timespec ts + ts.tv_sec = ms / 1000 + ts.tv_nsec = (ms % 1000) * 1000000 + nanosleep(addr(ts), null) +} + +fn parts(tm t) -> Date { + return Date(year: int(t.tm_year) + 1900, month: int(t.tm_mon) + 1, day: int(t.tm_mday), hour: int(t.tm_hour), + minute: int(t.tm_min), second: int(t.tm_sec), weekday: int(t.tm_wday), yearday: int(t.tm_yday) + 1) +} + +// the date and time of a moment, in UTC +fn date(int unix) -> Date { + tm t + i64 stamp = unix + gmtime_r(addr(stamp), addr(t)) + return parts(t) +} + +// the date and time of a moment, in the computer's time zone +fn local(int unix) -> Date { + tm t + i64 stamp = unix + localtime_r(addr(stamp), addr(t)) + return parts(t) +} + +// a moment as text, in the computer's time zone: %Y year, %m month, %d day, %H hour, %M minute, %S second +// (the rest of C's strftime works too: %A weekday name, %B month name, ...) +fn format(int unix, str pattern) -> str { + tm t + i64 stamp = unix + localtime_r(addr(stamp), addr(t)) + []char buf + for i in 0..256 { + buf.push(char(0)) + } + strftime(buf.ptr, 256, pattern, addr(t)) + return str(buf.ptr) +} + +// the number of days in a month (month is 1 to 12) +fn days_in_month(int year, int month) -> int { + if month == 2 { + if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) { + return 29 + } + return 28 + } + if month == 4 || month == 6 || month == 9 || month == 11 { + return 30 + } + return 31 +} diff --git a/tests/new/std_json.fch b/tests/new/std_json.fch new file mode 100644 index 0000000..5aa596b --- /dev/null +++ b/tests/new/std_json.fch @@ -0,0 +1,35 @@ +import json + +fn main() -> ! { + doc := try json.parse("{\"name\": \"Finch\", \"tags\": [\"fast\", \"small\"], \"stars\": 5, \"pi\": 3.25, \"ok\": true, \"none\": null, \"nested\": {\"deep\": [1, [2, 3]]}}") + print(doc.get("name").str_or(""), doc.get("tags").at(1).str_or(""), doc.get("stars").int_or(0), doc.get("pi").float_or(0.0)) + print(doc.get("ok").bool_or(false), doc.get("none").is_null(), doc.get("missing").is_null(), doc.has("name"), doc.has("nope")) + print(doc.get("nested").get("deep").at(1).at(0).int_or(-1), doc.get("tags").len(), doc.names()) + print(json.stringify(doc)) + + out := json.new_object() + out.set("greeting", json.new_text("hi \"there\"\n")) + list := json.new_list() + list.push(json.new_number(1)) + list.push(json.new_number(2.5)) + list.push(json.new_bool(false)) + list.push(json.new_null()) + out.set("values", list) + out.set("greeting", json.new_text("replaced")) + out.set("empty", json.new_object()) + print(json.stringify(out)) + print(json.pretty(out)) + + again := try json.parse(json.pretty(out)) + print(json.stringify(again) == json.stringify(out)) + print((try json.parse("\"caf\\u00e9 \\ud83d\\ude00 \\/\"")).str_or("")) + print((try json.parse(" [ ] ")).len(), (try json.parse("-12.5e1")).float_or(0.0)) + + for bad in ["{\"a\": }", "[1, 2", "{\"a\" 1}", "tru", "[1] x", "\"open", "", "[1,\n oops]"] { + json.parse(bad) or { + print(err) + continue + } + print("parsed?!", bad) + } +} diff --git a/tests/new/std_json.out b/tests/new/std_json.out new file mode 100644 index 0000000..d0eeb00 --- /dev/null +++ b/tests/new/std_json.out @@ -0,0 +1,26 @@ +Finch small 5 3.25 +true true true true false +2 2 ["name", "tags", "stars", "pi", "ok", "none", "nested"] +{"name":"Finch","tags":["fast","small"],"stars":5,"pi":3.25,"ok":true,"none":null,"nested":{"deep":[1,[2,3]]}} +{"greeting":"replaced","values":[1,2.5,false,null],"empty":{}} +{ + "greeting": "replaced", + "values": [ + 1, + 2.5, + false, + null + ], + "empty": {} +} +true +café 😀 / +0 -125 +json: found '}' where a value should be (line 1, column 7) +json: the text ends where ',' or ']' should be (line 1, column 6) +json: found '1' where ':' should be (line 1, column 6) +json: found 't' where 'true' should be (line 1, column 1) +json: found 'x' where the end of the text should be (line 1, column 5) +json: a text is never closed (line 1, column 6) +json: the text ends where a value should be (line 1, column 1) +json: found 'o' where a value should be (line 2, column 3) diff --git a/tests/new/std_path.fch b/tests/new/std_path.fch new file mode 100644 index 0000000..8fee317 --- /dev/null +++ b/tests/new/std_path.fch @@ -0,0 +1,10 @@ +import path + +fn main() { + print(path.join("a", "b.txt"), path.join("a/", "b"), path.join("", "x"), path.join("a", "/abs")) + print(path.base("/tmp/x/a.tar.gz"), path.base("a/b/"), path.base(""), path.base("/")) + print(path.dir("/tmp/x/a.txt"), path.dir("b.txt"), path.dir("/b"), path.dir("a/b/")) + print(path.ext("a.tar.gz"), path.ext("noext"), path.ext(".bashrc"), path.stem("/x/a.tar.gz")) + print(path.clean("a//b/../c/./d"), path.clean("/../a"), path.clean("../../a"), path.clean(""), path.clean("a/..")) + print(path.parts("/a//b/c/"), path.is_abs("/a"), path.is_abs("a")) +} diff --git a/tests/new/std_path.out b/tests/new/std_path.out new file mode 100644 index 0000000..0915fa1 --- /dev/null +++ b/tests/new/std_path.out @@ -0,0 +1,6 @@ +a/b.txt a/b x /abs +a.tar.gz b . / +/tmp/x . / a +.gz a.tar +a/c/d /a ../../a . . +["a", "b", "c"] true false diff --git a/tests/new/std_time_proc.fch b/tests/new/std_time_proc.fch new file mode 100644 index 0000000..d425dff --- /dev/null +++ b/tests/new/std_time_proc.fch @@ -0,0 +1,19 @@ +import time +import proc + +fn main() -> ! { + d := time.date(86400 * 365 + 3661) + print(d.year, d.month, d.day, d.hour, d.minute, d.second, d.weekday, d.yearday) + print(time.days_in_month(2024, 2), time.days_in_month(2023, 2), time.days_in_month(1900, 2), time.days_in_month(2000, 2), time.days_in_month(2024, 4)) + a := time.millis() + time.sleep(30) + waited := time.millis() - a + print(waited >= 25 && waited < 500) + print(time.now() > 1700000000, time.format(0, "%Y") == "1970" || time.format(0, "%Y") == "1969") + + print((try proc.output("echo hello; echo world")).trim()) + print(proc.run("exit 3")) + bad := proc.output("echo oops; exit 2") or err + print(bad) + print(proc.env("FINCH_SURELY_NOT_SET", "fallback"), proc.pid() > 0, proc.cwd().len > 0) +} diff --git a/tests/new/std_time_proc.out b/tests/new/std_time_proc.out new file mode 100644 index 0000000..5a5b4e5 --- /dev/null +++ b/tests/new/std_time_proc.out @@ -0,0 +1,9 @@ +1971 1 1 1 1 1 5 1 +29 28 28 29 30 +true +true true +hello +world +3 +'echo oops; exit 2' exited with code 2: oops +fallback true true