Unreleased
In developmentAdded
- Smart module system:
#importnow resolves user modules semantically. Every form is supported — namespace (#import math;), wildcard (#import math.*;), single symbol (#import math.PI;), selected symbols with per-symbol aliases (#import math.{a, b as c};), namespace aliases (#import math as m;), submodule paths (#import shapes.vec;), and quoted file imports (#import "lib/tools.tnc" as t;). Paths resolve relative to the importing file;a.b.cfindsa/b/c.tnc, then the directory formsa/b/c/mod.tnc/a/b/c/c.tnc— directories become modules automatically (shapes/mod.tncis the moduleshapes). Imports load recursively with cycle detection and cache by absolute path, so diamond imports share one module instance; re-importing the same module is idempotent. Module-name collisions andstd.*imports (not available yet) get clear diagnostics. modulekeyword:module name;names a file's module (files without one take their file stem);module name { ... }groups declarations into an in-file namespace, with nesting (module a { module b { ... } }→a.b.member) and dotted names (module a.b { ... }).pubmarks exports; private items are rejected when imported (symbol x is private to module math).- Generics end-to-end:
fn name:T(...),struct Name:T { ... }, andalias Name:T = ...;(including multi-param:(K, V)forms) are monomorphized — each concrete type-argument set produces one copy with a mangled C name (Pair:i32→tnc_Pair_i32), cached per instance. Calls support explicit type arguments (identity:str(x)) and inference from argument types (identity(x)), and generics work across module boundaries (shapes.Circle:f64,math.identity:i32(42)). - Generics importable by name: pub generic fn/struct/alias templates bind through every import form like plain symbols —
#import math.identity;(bareidentity:i32(42)/ inferredidentity(7)),#import math.identity as id;,#import math.{Pair};(barePair:f64 { ... }),#import box.Opt;, and#import math.*;binds every pub generic bare. A bare instantiation and the qualified call share one mangled C instance (identity:i32(42)andmath.identity:i32(42)emit a singletnc_math_identity_i32); private generics reportsymbol x is private to module mathon both paths; module-block generics are reachable through dotted chains (math.physics.blockid:i32(42)). - Generic bodies compose: a generic fn/method body may reference the defining module's own generics (
makePair:(K, V)returningPair:(K, V) { ... }) — instantiated bodies have their type expressions substituted too, and are checked against the defining module's analyzer, so module-private helpers and consts resolve exactly as in the defining file. - Single merged C output: every loaded module compiles into one C translation unit in load order (imports before importers), with type typedefs, prototypes, and file-scope data hoisted so call order across modules never matters; module items get mangled C names (
math.abs→tnc_math_abs) so same-named items never collide. - Sample module demo:
samples/modules/— a multi-file demo (main.tnc,math.tnc,shapes/mod.tnc,shapes/vec.tnc) exercising every import form, themodulekeyword (declaration + block), directory modules, and generics across modules;samples/build.shruns it alongside the single-file samples. - Docs:
syntax.md's Modules and Preprocessor sections now document the full#importgrammar, themodulekeyword,pub/private visibility, directories-as-modules, and module resolution rules, plus a new C Interop section covering#importc(previously undocumented) andextern "C" fn; the language-support table reflects the module system and generics. - Optionals:
?T— a{ T value; bool has_value; }wrapper emitted as a named typedef (?i32→tnc_opt_i32), withnullas the empty value,x orelse fallbackdefaulting (the fallback is only evaluated when the optional is null),x?payload unwrap, and== null/!= nullpresence checks. A plainTvalue (ornull) auto-wraps into a?Twherever one is expected — initializers, call arguments, return values, assignments — and untyped literals adapt to the payload type (var x ?f64 = 5;). Comparing an optional with a non-null value,orelseon a non-optional,?on a non-optional, and optionals of arrays orvoidare all rejected with clear diagnostics. Sample:samples/23_optionals.tnc. - Arrays:
[N]T,[_]T(size inferred from the literal), and[N:x]Tsentinel-terminated arrays — array literals with inferred or explicit element types, index access and element assignment (arr[i],arr[i] = v), the.lenproperty (compile-time constant for arrays), andfor arr |x|collection iteration. Multidimensional row-major arrays ([4][5]f32) support chained indexing (mat[i][j]) and row-by-row iteration; iterating a multidimensional array directly is rejected with a hint. - Slices:
[]T— a fat pointer{ ptr: ^T, len: usize }that views an array's storage. Arrays convert implicitly to slices at call sites and in assignments, so functions take collections as slices (fn total(s []i32)), index and mutate through them (write-through to the backing array), read.lenat runtime, and iterate withfor s |v|. Array parameters, array returns, and whole-array assignment are rejected with a "use a slice" diagnostic; slice types are emitted as named typedefs (tnc_slice_i32) so prototypes and definitions share one C type. - Samples:
samples/19_array_basics.tncthroughsamples/22_array_functions.tnccovering array literals/types, slices and implicit conversion, multidimensional + sentinel arrays, and functions over collections. - Docs:
syntax.md's Array section (previously a placeholder) now documents arrays, slices, multidimensional arrays, and sentinel-terminated arrays with working examples. tinoc version/tinoc help: the CLI now reports project metadata — the GitHub repository (tinoc-lang/tinoc), the creator's GitHub (pbarot2009), the official website, and the license — intinoc version, theLinkssection oftinoc help, and the newtinoc help versionscreen.- Enums:
enum Name { Variant, Other(type), ... }— fieldless variants compile to plain C enums; variants with payloads become tagged unions (tag + per-variant anonymous struct), so multi-field payloads never overlap. Enums support instance methods (self ^Name), static methods, equality, and being passed/returned like any other type. - Enum
switch: exhaustive enum switches (all variants listed, no_arm needed) with pattern binding —Shape.Rect(w, h) => { ... }binds the payload fields directly;_wildcards discard a payload slot. Missing-return analysis understands exhaustive enum switches. - Unions:
union Name { field type; ... }with C-style shared-memory fields — writingas_intand readingas_floatreinterprets the same bytes (IEEE-754 punning works end-to-end). Unions reuse struct field syntax, support instance methods (self ^Name) and static methods, and reject comparisons/arithmetic with clear diagnostics. - Samples:
samples/14_enum_basics.tncthroughsamples/17_str_strings.tnccovering fieldless enums, tagged unions with pattern matching, enum methods, andstrsemantics, plussamples/18_combo.tnc, which exercises structs, enums (pattern matching), unions (type punning),str,switch, and methods all in one program. - Installers:
install.sh/install.ps1fetch the latest GitHub release, verify it against the releaseSHA256SUMSmanifest, and extract it into~/.tinoc/— withVERSIONtracking, update checks that ask before upgrading,--localsource builds,--check,--uninstall, and--version/--force/--dirflags. - CI workflow (
.github/workflows/ci.yml) runs on every push and pull request: deps, fmt-check, vet, lint, race tests, and a release build (build.sh ci/build.ps1 ci) across ubuntu/macos/windows, plus an installer smoke test in--localmode. - The release workflow now attaches a
SHA256SUMSmanifest to every release, so installers can verify downloads. .gitattributes: Go and shell sources are pinned to LF line endings on checkout everywhere, sogofmt -lno longer false-fails on Windows CI (where git used to check out CRLF).
Fixed
#importclocal headers next to modules in subdirectories: a module file importing its own local header (#importc "vecmath.h"insidelib/mathc.tncwithvecmath.hbeside it) emitted the right quoted include into the merged C, but the C compile step only put the entry file's directory on the include path — so the header was not found and the build failed.compileGeneratedCnow receives every loaded module's directory as an-Ipath (in load order, deduplicated), so local headers resolve wherever they live.- Generic struct instantiation recursion: a generic method whose signature references the struct's own type (
fn sum(self ^Pair:T)) re-entered instantiation while the instance was still being built and recursed forever (clone → register method → resolve signature → re-instantiate). Instances are now cached before their method signatures resolve, so the re-entrant call hits the cache. - Generic bodies kept stale type parameters: after monomorphization a generic body's type expressions still carried the template's type parameters (
return Pair:(K, V) { ... }inside amakePair:(K, V)instance), so re-checking the body failed on K/V.substituteBodyTypesnow rewrites every reachable type expression in the cloned body from the same substitution environment as the signature. - Cross-module generic instances checked against the caller: method and fn bodies of a generic instantiated from another module were checked in the caller's scope, so module-private names referenced by the body did not resolve. Instances now carry the defining module's analyzer, and the caller's concrete type arguments are mirrored into it, so bodies resolve module-local helpers/consts and caller-provided types alike.
- Stale
#importcparse cache: the disk cache keyed only on the wrapper text and dumper identity, so two#importc "vecmath.h"from different directories (or an edited local header) could be served the stale parse of a different file under the same name — silently missing or wrong declarations. The key now folds in each local header's resolved absolute path plus a hash of its current contents, so same-named headers across directories and edits always re-parse. - Module-file blocks unreachable from importers: a
module name { ... }block inside a module file (module math;…module physics { pub const g; }) could not be reached cross-module —math.physics.gfailed with "module math has no public member physics". Blocks now publish a pub-only projection into the file module's namespace, so dotted chains (math.physics.g,math.physics.weight) resolve, nested blocks chain (math.a.b.VAL), and private block members stay file-private with a clear diagnostic. - Private
const/vardiagnostics: importing a private top-levelconst/varreported "module x has no public symbol y"; it now reports "symbol y is private to module x", matching private functions. Private globals are tracked per module and checked in both the symbol-import and qualified-access paths. - Module
strglobals: top-levelconst/varof typestr(or arrays/slices ofstr) in module files emitted invalid C (static const str x = tinoc_str_lit(...)— a function call in a static-storage initializer). Literals now emit a constant brace initializer ({ .data = "...", .len = N }); other str initializers fall back to external linkage. This broke wildcard-imported string constants. - Nested module blocks:
module a { module b { pub const VAL; } }failed to resolvea.b.VAL— block views registered only under their local segment, so dotted chains through nested/dotted block namespaces now register every dotted prefix of the full name. - Dead module globals: unreferenced private top-level
const/varin module files are no longer emitted into the merged C output (they were always emitted asstatic, making the C compiler warn with-Wunused-const-variable/-Wunused-variableand padding the output with dead data). A global is kept when it ispub(reachable through its module namespace, e.g.math.PI), amodule name { ... }block member, or referenced by a bare identifier anywhere in the compilation. - CI / C-interop tests: the
#importcerror-checking tests (TestCImport_UndefinedMember,TestCImport_WrongArgCount,TestCImport_WrongArgType,TestCImport_DuplicateAlias) now skip when no clang or gcc is available to parse C headers, matching the rest of the suite (requireCC,TestCImport_ExternVars). Previously they hard-failed on runners without a C toolchain — e.g. the Windows CI runner — which brokebuild.sh ci/build.ps1 cion both push and pull-request events. tinoc runno longer leaves binaries behind: the compiled binary is written to a temporary work directory and removed as soon as the program finishes, sotinoc runleaves no artifacts in the working directory (pass-o <path>to keep one). The generated-C scratch directory is also cleaned up after bothbuildandrun— it previously leaked in the system temp dir on every invocation.- Float literals: underscore digit separators (e.g.
1_000.5) are now stripped before emission into C, so float literals with separators no longer generate C that fails to compile. - For-collection loops: the loop index is compared against the collection length as
size_t, eliminating a-Wsign-comparewarning when iterating slices (and the generated loop now binds the collection once, evaluating it exactly once). - Slice
.len: typed asi32soi < s.lentype-checks; codegen casts the Csize_tfield so user comparisons do not trigger-Wsign-compare. - Const arrays: iterating a
constarray no longer emits a-Wdiscarded-qualifierswarning (the loop uses a const pointer). - Compiler: calling an instance method that was declared without a
selfparameter (e.g.fn make() Data { ... }inside a struct/union body, invoked asd.make()) previously panicked with an index-out-of-range error; it now reports a proper "needs a self parameter" diagnostic. str:<,>,<=,>=onstrare now rejected with a clear diagnostic (only==/!=are defined, via content comparison) instead of emitting invalid C. Switching on astris likewise a proper semantic error.str ==/!=compare by content through thetinoc_str_eqruntime helper.- Installers:
--localno longer prompts to "update" when the built version equals the installed version — the fresh source build is installed directly. Re-running the installer with the latest version already installed now exits before downloading anything. - Installers:
--force/--yesnow actually reinstalls over an identical installed version instead of reporting "already installed". - CLI: color output is only emitted when stdout is a real terminal, so piped commands like
tinoc version | awk ...return plain text (this was breaking the installer's version parsing on systems with TERM set). - Version reporting:
build.sh/build.ps1now inject the version intosrc.Versionvia-ldflags(the old-X main.versiontarget never existed, so release binaries always reported the hardcoded0.1.0). - Installers: installed-version comparisons normalize a leading
v, so aVERSIONfile written asv0.1.0(e.g. by an earlier installer) is correctly treated as equal to0.1.0instead of triggering a spurious "update available" prompt. - Installers:
--verbosenow prints the download/fetch commands it runs. - Installers: confirmation prompts now decline gracefully when there is no interactive terminal (CI, piped input, cron) instead of aborting the whole install with an error — the binary still installs and manual PATH instructions are printed.
- Installers: the update prompt now appears before the download starts, so a declined update never wastes bandwidth on a full release download.
install.ps1now detects the real OS and architecture (linux/darwin/windows × amd64/arm64) and installs the matching binary name, making it fully cross-platform under PowerShell 7+ — matchinginstall.sh. It also prefers the repo'sbuild.shwhen running-Localon macOS/Linux.build.ps1: OS/architecture detection now matchesbuild.sh, soinstall.ps1 -Localon macOS/Linux cross-targets correctly.install.sh: archive binary discovery no longer relies on the GNU-onlyfind -maxdepth, which errored on macOS's BSD find; it also offers to create a missing shell rc file when adding~/.tinoc/binto PATH.