Is BASIC Interpreted or Compiled? And Can It Compete With C?

January 14, 2026

 

Logo

Is BASIC Interpreted or Compiled? And Can It Compete With C?

This question comes up a lot, especially from programmers who cut their teeth on early home computers:

 *Is BASIC interpreted or compiled? And if it’s compiled, can it ever approach the performance of C or C++?*

The short answer is: yes, it can—but the longer answer is far more interesting.


From BASIC to Native Code

When people talk about “compiled BASIC,” they often imagine a direct leap from BASIC source code to machine code. In practice, that’s rarely how modern systems work.

It’s not uncommon—for BASIC or any language—to compile into an intermediate form first. Historically this might be bytecode, but today it could also be an abstract syntax tree or another IR (intermediate representation). From there, many systems transpile into C, LLVM IR, or another mature backend.

Why does this matter?

Because those backends are battle-tested. They encapsulate decades of optimization knowledge, and by targeting them, a BASIC compiler can automatically benefit from highly sophisticated optimization passes—without reinventing the wheel.


Generating Native Code: Several Paths

There are a few common approaches BASIC compilers have taken over the years:

  • Direct machine code generation
  • Possible, but generally not recommended unless you enjoy pain.

  • Assembly generation
  • A more common approach: translate bytecode into assembly, then feed it through an embedded assembler.

  • C or modern IR backends
  • Increasingly popular, and often the most pragmatic choice.

    The good news is that even a naive bytecode-to-assembly translator can produce code that runs quite well. At its simplest, this is often a near line-for-line mapping of bytecode instructions into native instructions.

    That kind of output tends to hit memory a lot—but even so, it already executes far faster than an interpreter.


    Where Performance Is Really Won (or Lost)

    Here’s where things get interesting.

    Most people think performance comes down to low-level micro-optimizations. Those matter—but they’re not where the biggest gains usually come from.

    The real performance cliff is often created much earlier, by language and runtime design decisions.

    Memory Access Matters

    A naive translation model tends to generate excessive memory loads and stores. Simply removing redundant memory accesses can result in large performance wins.

    Many BASIC compilers already perform instruction-level optimizations at the bytecode stage:

  • Removing redundant loads and stores
  • Eliminating dead code
  • Collapsing simple instruction sequences
  • Even modest cleanup here can produce surprisingly large gains.


    The Hidden Cost of “Convenience”

    One of the classic examples is string handling.

    In the BASIC world, string systems are often built from off-the-shelf components designed for flexibility and safety, not speed. They work—and they work well—but they can absolutely destroy performance if you’re not careful.

    This isn’t a BASIC-only problem. It’s a reminder that:

  • Performance isn’t just about the compiler.
  • It’s about how the language chooses to represent and manage data.

  • Can BASIC Match C or C++?

    The uncomfortable truth for some people is this:

    Yes—BASIC compilers can generate code that rivals (or even beats) the output of some C compilers.

    Remember:

  • Not all C compilers are equal
  • Not all C code is well-written
  • And not all optimizers are created equal
  • That said, most BASIC compilers don’t aim for absolute peak performance. Their goals are often different: approachability, safety, rapid development, or portability.

    But the idea that there’s some enormous, unbridgeable performance chasm between BASIC and C is largely a relic of the interpreter era.

    A lot has changed since then.


    Final Thoughts

    The real takeaway is this:

  • BASIC doesn’t have to be slow
  • Compilation strategies matter
  • Runtime design matters even more
  • Modern compilation techniques have blurred the old lines. Performance today is less about what language you use and more about how that language is implemented.

    And that’s a far more interesting conversation than “interpreted vs compiled” ever was.


    Manual Base Conversion in PlayBASIC

    December 08, 2025

     

    Logo

    Converting a decimal number stored as a string into Binary, Octal and Hexadecimal


    In this tutorial we are going to manually convert a decimal number stored inside a string into:

    • Base 2 (Binary)

    • Base 8 (Octal)

    • Base 16 (Hexadecimal)

    This example avoids built-in conversion commands on purpose, so beginners can see how the process works internally.


    Example Output Usage

    s$="87654321"
    print s$ +"="+ ConvertTo(S$,2)
    print s$ +"="+ ConvertTo(S$,8)
    print s$ +"="+ ConvertTo(S$,16)
    print ""
    
    s$="-12345678"
    print s$ +"="+ ConvertTo(S$,2)
    print s$ +"="+ ConvertTo(S$,8)
    print s$ +"="+ ConvertTo(S$,16)
    print ""
    
    s$="255"
    print s$ +"="+ ConvertTo(S$,2)
    print s$ +"="+ ConvertTo(S$,8)
    print s$ +"="+ ConvertTo(S$,16)
    print ""
    
    Sync
    waitkey

    Step 1: Manually Converting the String to an Integer

    Before we can convert to another base, we must first turn the string into an actual integer value.

    This is done digit-by-digit using basic decimal math.

    Function ConvertTo(S$,Base)
    rem assumed 32bit integers
    Total =0
    Negate=0
    
    for lp=1 to len(s$)
        Total=Total*10
        ThisCHR = asc(mid$(s$,lp))
    
        if ThisChr = asc("-") then Negate=1   
    
        if ThisChr >= asc("0") and ThisCHR<=asc("9")
            Total=Total+(ThisCHR-Asc("0"))       
        endif
    next
    
    if Negate then Total *= -1   

    What’s happening here?

    • Each digit is multiplied into place using base-10 math

    • `ASC()` is used to convert characters into numeric values

    • The minus symbol `"-"` is detected and applied at the end

    This is essentially how a basic `Val()` function works internally.


    Step 2: Preparing for Base Conversion

    Each output base is selected using bit grouping.

    select base
    case 2
    Shift=1
    Characters$="01"
    case 8
    Shift=3
    Characters$="01234567"
    case 16
    Shift=4
    Characters$="0123456789ABCDEF"
    endselect

    Why these values?

    • Binary uses 1 bit per digit

    • Octal uses 3 bits per digit

    • Hexadecimal uses 4 bits per digit


    Step 3: Bitwise Conversion Loop

    Now the number is converted using bit masking and bit shifting.

    if Shift
    Mask    = (2^Shift)-1
    Digits = 32 / Shift
    
        For lp=0 to Digits-1
            ThisCHR = Total and MASK
            Result$ = Mid$(Characters$,ThisChr+1,1) + Result$
            Total = Total >> Shift                               
        next
    endif
       
    
    EndFunction Result$

    Important notes:

    • Output is a fixed 32-bit representation

    • Leading zeros are expected and correct

    • Negative numbers are shown using two’s complement

    The result string is built from right to left because the least-significant bits are processed first.


    Summary

    This tutorial demonstrates:

    • Manual string → integer conversion

    • Decimal positional maths

    • Bit masking and shifting

    • Why binary, octal and hex exist

    • How CPUs naturally represent numbers

    This approach may not be the shortest, but it clearly shows how the conversion works under the hood — making it ideal for learners.

    Complete Code:

        s$="87654321"
        print s$ +"="+ ConvertTo(S$,2)
        print s$ +"="+ ConvertTo(S$,8)
        print s$ +"="+ ConvertTo(S$,16)
        print ""
    
        s$="-12345678"
        print s$ +"="+ ConvertTo(S$,2)
        print s$ +"="+ ConvertTo(S$,8)
        print s$ +"="+ ConvertTo(S$,16)
        print ""
    
        s$="255"
        print s$ +"="+ ConvertTo(S$,2)
        print s$ +"="+ ConvertTo(S$,8)
        print s$ +"="+ ConvertTo(S$,16)
        print ""
    
        Sync
        waitkey
       
    
    Function ConvertTo(S$,Base)
        rem assumed 32bit integers
        Total =0
        Negate=0
        for lp=1 to len(s$)
            Total    =Total*10
            ThisCHR = mid(s$,lp)
            if ThisChr = asc("-") then Negate=1   
            if ThisChr >= asc("0") and ThisCHR<=asc("9")
                Total=Total+(ThisCHR-Asc("0"))       
            endif
        next
        if Negate then Total *= -1   
    
        Characters$    ="0123456789ABCDEF"
        select base
                    case 2
                        Shift=1   
                    case 8
                        Shift=3   
                    case 16
                        Shift=4   
        endselect   
       
        if Shift
            Mask        =(2^Shift)-1
            For lp=1 to 32 / Shift
                    ThisCHR = Total and MASK
                    Result$ = Mid$(CHaracters$,ThisChr+1,1) +Result$
                    Total = Total >> Shift                               
            next
        endif
           
    EndFunction Result$

    Let’s Write a Lexer in PlayBASIC

    October 12, 2025

     

    Logo

    Introduction

    Welcome back, PlayBASIC coders!

    In this live session, I set out to build something every programming language and tool needs — a lexer (or lexical scanner). If you’ve never written one before, don’t worry — this guide walks through the whole process step by step.

    A lexer’s job is simple: it scans through a piece of text and classifies groups of characters into meaningful types — things like words, numbers, and whitespace. These little building blocks are called tokens, and they form the foundation for everything that comes next in a compiler or interpreter.

    So, let’s dive in and build one from scratch in PlayBASIC.


    Starting with a Simple String

    We begin with a test string — just a small bit of text containing words, spaces, and a number:

    s$ = "   1212123323      This is a message number"
    Print s$

    This gives us something to analyze. The plan is to loop through this string character by character, figure out what each character represents, and then group similar characters together.

    In PlayBASIC, strings are 1-indexed, which means the first character is at position 1 (not 0 like in some other languages). So our loop will run from 1 to the length of the string.


    Stepping Through Characters

    The core of our lexer is a simple `For/Next` loop that moves through each character:

    For lp = 1 To Len(s$)
        ThisCHR = Mid(s$, lp)
    Next

    At this stage, we’re just reading characters — no classification yet.

    The next question is: how do we know what type of character we’re looking at?


    Detecting Alphabetical Characters

    We start by figuring out if a character is alphabetical. The simplest way is by comparing ASCII values:

    If ThisCHR >= Asc("A") And ThisCHR <= Asc("Z")
        ; Uppercase
    EndIf
    
    If ThisCHR >= Asc("a") And ThisCHR <= Asc("z")
        ; Lowercase
    EndIf

    That works, but it’s messy to write out in full every time. So let’s clean it up by rolling it into a helper function:

    Function IsAlphaCHR(ThisCHR)
        State = (ThisCHR >= Asc("a") And ThisCHR <= Asc("z")) Or _
                (ThisCHR >= Asc("A") And ThisCHR <= Asc("Z"))
    EndFunction State

    Now we can simply check:

    If IsAlphaCHR(ThisCHR)
        Print Chr$(ThisCHR)
    EndIf

    That already gives us all the letters from our string — but one at a time.

    To make it more useful, we’ll start grouping consecutive letters into words.


    Grouping Characters into Words

    Instead of reacting to each character individually, we look ahead to find where a run of letters ends. This is done with a nested loop:

    If IsAlphaCHR(ThisCHR)
        For ChrLP = lp To Len(s$)
            If Not IsAlphaCHR(Mid(s$, ChrLP)) Then Exit
            EndPOS = ChrLP
        Next
        ThisWord$ = Mid$(s$, lp, (EndPOS - lp) + 1)
        Print "Word: " + ThisWord$
        lp = EndPOS
    EndIf

    Now our lexer can detect whole words — groups of letters treated as a single unit.

    That’s the first real step toward tokenization.


    Detecting Whitespace

    The next type of token is whitespace — spaces and tabs.

    We’ll build another helper function:

    Function IsWhiteSpace(ThisCHR)
        State = (ThisCHR = Asc(" ")) Or (ThisCHR = 9)
    EndFunction State

    Then use the same nested-loop pattern:

    If IsWhiteSpace(ThisCHR)
        For ChrLP = lp To Len(s$)
            If Not IsWhiteSpace(Mid(s$, ChrLP)) Then Exit
            EndPOS = ChrLP
        Next
        WhiteSpace$ = Mid$(s$, lp, (EndPOS - lp) + 1)
        Print "White Space: " + Str$(Len(WhiteSpace$))
        lp = EndPOS
    EndIf

    Now we can clearly see which parts of the string are spaces and how many characters each whitespace block contains.


    Detecting Numbers

    Finally, let’s detect numeric characters using another helper:

    Function IsNumericCHR(ThisCHR)
        State = (ThisCHR >= Asc("0")) And (ThisCHR <= Asc("9"))
    EndFunction State

    And apply it just like before:

    If IsNumericCHR(ThisCHR)
        For ChrLP = lp To Len(s$)
            If Not IsNumericCHR(Mid(s$, ChrLP)) Then Exit
            EndPOS = ChrLP
        Next
        Number$ = Mid$(s$, lp, (EndPOS - lp) + 1)
        Print "Number: " + Number$
        lp = EndPOS
    EndIf

    Now we can identify three types of tokens:

    Words (alphabetical groups)

    Whitespace (spaces and tabs)

    Numbers (digits)


    Defining a Token Structure

    Up to this point, our program just prints what it finds.

    Let’s store these tokens properly by defining a typed array.

    Type tToken
        TokenType
        Value$
        Position
    EndType
    Dim Tokens(1000) As tToken

    We’ll also define some constants for readability:

    Constant TokenTYPE_WORD        = 1
    Constant TokenTYPE_NUMERIC     = 2
    Constant TokenTYPE_WHITESPACE  = 4

    As we detect tokens, we add them to the array:

    Tokens(TokenCount).TokenType = TokenTYPE_WORD
    Tokens(TokenCount).Value$    = ThisWord$
    TokenCount++

    Do the same for whitespace and numbers, and our lexer now builds a real list of tokens as it runs.


    Displaying Tokens by Type

    To visualize the result, we can print each token in a different colour:

    For lp = 0 To TokenCount - 1
        Select Tokens(lp).TokenType
            Case TokenTYPE_WORD:       c = $00FF00 ; green
            Case TokenTYPE_NUMERIC:    c = $0000FF ; blue
            Case TokenTYPE_WHITESPACE: c = $000000 ; black
            Default:                   c = $FF0000
        EndSelect
    
        Ink c
        Print Tokens(lp).Value$
    Next

    When we run this version, we see numbers printed in blue, words in green, and whitespace appearing as black gaps — exactly how a simple syntax highlighter or compiler front-end might visualize tokenized text.


    Wrapping Up

    And that’s it — our first lexer!

    It reads through a line of text, classifies what it finds, and records each token type for later use.

    The same process underpins many systems:

    Compilers use it as the first step in parsing code.

    Adventure games might use it to process typed player commands.

    Expression evaluators or script interpreters rely on it to break down formulas and logic.

    The big takeaway? A lexer doesn’t have to be complicated.

    This simple approach — scanning text, detecting groups, and tagging them — is the heart of it. Once you understand that, you can expand it to handle symbols, punctuation, operators, and beyond.

    If you’d like to see more about extending this lexer or turning it into a parser, let me know in the comments — or check out the full live session on YouTube.

    Links:

  • PlayBASIC,com
  • Learn to basic game programming (on Amazon)
  • Learn to code for beginners (on Amazon)