SBim

Descripción del contenido de la página

Preprocesador para S*BASIC

Etiquetas:

«SBim» significa «S*BASIC improved», esto es, «S*BASIC mejorado». «S*BASIC» quiere decir tanto SuperBASIC (el BASIC original de Sinclair QL) como su versión mejorada SBASIC.

SBim es por una parte un formato mejorado para escribir código en S*BASIC; por otra, es un preprocesader escrito en Vim que traduce el código fuente al formato propio de S*BASIC, listo para ser interpretado.

El formato SBim

Mejoras que aporta el formato SBim respecto al S*BASIC original:

Comentarios

En SBim puede usarse las comillas simples para los comentarios de línea, como es habitual en otras versiones de BASIC. Estos comentarios serán eliminados durante la conversión del código a S*BASIC.

Estos comentarios pueden estar al inicio de la línea (precedidos opcionalmente por espacios o tabuladores), siempre que estén seguidos por un espacio (o nada). Si no están al inicio de la línea deben estar precedidos por un espacio y seguidos por otro espacio (o nada).

Por supuesto, los comentarios de línea propios de S*BASIC, con rem o REMark, pueden seguir usándose y serán conservados.

Líneas partidas

Una línea de código fuente puede ser dividida en más de una línea de texto. SBim usa una barra inclinada hacia la izquierda («\»), como es habitual en diversos lenguajes de programación.

En S*BASIC «\» es un separador de impresión que en ocasiones puede quedar al final de la línea de código. Para evitar la ambigüedad en dichos casos se puede añadir el separador de instrucciones, los dos puntos («:»).

La indentación de las líneas de texto segunda y siguientes de una línea de código dividida no es tenida en cuenta, será eliminada en la línea de código resultante. Pero los espacios antes del signo «\» sí se conservarán, y a menudo son imprescindibles, como muestra el siguiente ejemplo, en que también se ilustra el uso original de «\» como operador de impresión:

let \
  d=18871215
print\
  "Saluton, \
  mondo!":\
list
print\ \
  "Bonan matenon"
print\:
clear

El código anterior convertido a S*BASIC:

1 let d=18871215
2 print "Saluton, mondo!":list
3 print\"Bonan matenon"
4 print\:
5 clear

Etiquetas

En SBim se usan etiquetas en lugar de números de línea.

Los nombres de las etiquetas deben empezar siempre por el signo de la arroba, tanto al definirlas como al referenciarlas. El resto de caracteres pueden ser letras del código ASCII, dígitos o el signo de subrayado («_»), en cualquier orden o combinación. No se hace distinción entre minúsculas y mayúsculas. No hay longitud máxima para el nombre de una etiqueta (salvo la impuesta por el lenguaje de programación de Vim, cualquiera que sea).

Para definir una etiqueta basta que su nombre esté al comienzo de una línea de código (puede haber espacios o tabuladores antes):

gosub @routine1
stop

@routine1
  print "Routine 1"
  @wait
  if not rnd then goto @wait
  return

Para mayor claridad, puede usarse el comando label para definir las etiquetas:

gosub @routine1
stop

label @routine1
  print "Routine 1"
  label @wait
  if not rnd then goto @wait
  return

Cualquier palabra que en el código fuente cumpla las condiciones para ser una etiqueta será tomada como tal, independientemente de su posición en el código. Esto permite incluir etiquetas dentro de cadenas o en cálculos, como haríamos con los propios números de línea:

restore @my_data+index

Sin embargo eso puede tener efectos indeseados. Por ejemplo, dado el siguiente código:

label @blabla
print "Hola"
print "Mi correo-e es: superbasic@blablabla.info"
print "Y mi otro correo-e es: superbasic@blabla.info"

El resultado final en S*BASIC sería el siguiente, con una sustitución indeseada en la última línea:

2 print "Hola"
3 print "Mi correo-e es: superbasic@blablabla.info"
4 print "Y mi otro correo-e es: superbasic1.info"

Para evitar esas posibles coincidencias debe usarse algún medio de camuflar la falsa etiqueta, como en los ejemplos siguientes:

rem Arroba camuflada:
print "Y mi otro correo-e es: superbasic";chr$(64);"blabla.info"
rem Arroba separada:
print "Y mi otro correo-e es: superbasic@";"blabla.info"

Primer número de línea

Puede definirse el primer número de línea que se usará para renumerar el programa final, con el comando #firstline, que debe estar al inicio de cualquier línea del programa (los espacios o tabuladores previos no serán tenidos en cuenta):

#firstline 9000

Si hay más de un comando #firstline solo el primero será tenido en cuenta. Si no se usa este comando, el primer número de línea será el uno.

Inclusión de otros ficheros fuente

Es posible incluir otros ficheros fuente dentro del principal con la directiva #include:

#include lib/routine.bas

A su vez, el fichero incluido puede contener otras directivas #include.

Metaetiquetas y conversión condicional

Ejemplo
#define debugging
#define qdos

#ifdef debugging
  print "I'm debugging"
#endif

#ifdef qdos

  ' QDOS version

#else

  ' SMSQ/E version

#endif


Posibles mejoras

Entre las ideas consideradas para futuras versiones del formato SBim están las siguientes:

Instalación

Para instalar SBim basta ejecutar make install en el directorio del programa. Los ficheros propios de Vim se instalarán en <~/.vim>, pero el comando sbim se copiará en <~/bin>. Si se desea usar en su lugar, hay que editar el valor de la variable BINDIR en el fichero . Para desinstalar, make uninstall.

Uso

Cuando se usa el editor Vim para editar ficheros con extensión «.sbim», o ficheros que incluyen una línea de configuración de Vim con el comando filetype=sbim, estará disponible el atajo de teclado _bas para convertir el código en formato estándar de S*BASIC. El resultado será un fichero con el mismo nombre que el actual y en su mismo directorio, pero con la extensión _bas añadida.

SBim puede usarse también como un comando, en cuyo caso puede indicarse el nombre del fichero de salida. Ejemplos:

sbim inputfile.bas
sbim inputfile.bas outputfile.bas

Código fuente

El fichero principal de SBim es el conversor de código:

" sbim.converter.vim
" (~/.vim/sbim.vim)

" This file is part of SBim
" http://programandala.net/es.programa.sbim.html

" Last modified 201710181334
" See change log at the end of the file

" ==============================================================
" Description

" This VimL program converts S*BASIC source code written in the
" SBim format to an ordinary S*BASIC file ready to be loaded by
" a S*BASIC interpreter.

" ==============================================================
" Author and license

" Author: Marcos Cruz (programandala.net), 2011, 2012, 2015,
" 2016, 2017

" You may do whatever you want with this work, so long as you
" retain the copyright/authorship/acknowledgment/credit
" notice(s) and this license in all redistributed copies and
" derived works.  There is no warranty.

" ==============================================================

if exists("*SBim")
  " Function `SBim` is already defined.
  finish
endif

" ==============================================================
" Generic functions {{{1

function! Trim(input_string)
  " Remove trailing spaces from a string.
  " Reference:
  " http://stackoverflow.com/questions/4478891/is-there-a-vimscript-equivalent-for-rubys-strip-strip-leading-and-trailing-s
  return substitute(a:input_string, '^\s*\(.\{-}\)\s*$', '\1', '')
endfunction

" ==============================================================
" Cleaning {{{1

function! SBimClean()

  silent %s,\t\+, ,ge " Remove tabs
  echo 'Tabs removed.'

  silent %s,\(^\s*\|\s\+\)'\(\s.*\)\?$,,e " Remove line comments
  echo 'Comments removed.'

  silent %s,^\s*\n,,ge " Remove the empty lines
  echo 'Empty lines removed.'

  silent %s,^\s*\(.\+\)\s*$,\1,e " Remove blanks
  echo 'Indentation and blanks removed.'

  silent %s,\\\s*\n,,e " Join the splitted lines 
  echo 'Splitted lines joined.'

endfunction

" ==============================================================
" Renum

function! SBimRenum()

  " Put line numbers;
  " remove spaces from blank lines;
  " save the file.

  " Call the the nl program (part of the Debian coreutils package):
  execute ":silent %!nl --body-numbering=t --number-format=rn --number-width=5 --number-separator=' ' --starting-line-number=".s:renumLine." --line-increment=1"

  " In older versions of coreutils,
  " -v sets the first line number, and -i sets the line increment.
  " (the long option for -v doesn't work, though the manual mentions it).
  " Modern versions of nl uses the clearer options
  " --first-line and --line-increment, see:
  " http://www.gnu.org/software/coreutils/manual/coreutils.html#nl-invocation

  " Remove all spaces from lines that only contain spaces:
  ":%s/^\s\+$//

  " Remove spaces before line numbers
  "(nl has no option to remove them):
  silent %substitute/^\s*//e

  echo 'Line numbers added.'

endfunction

function! SBimGetRenumLine()

  " Store into s:renumLine the first line number
  " to be used by the final S*BASIC program
  " The directive `#renum` can be used to set
  " the desired line number. Only the first occurence
  " of `#renum` will be used; it can be anywhere
  " in the source but always at the start of a line
  " (with optional indentation).

  let s:renumLine=1 " default value

  call cursor(1,1)
  if search('^\s*#renum\s\+[0-9]\+\>','Wc')
    let l:valuePos=matchend(getline('.'),'^\s*#renum\s*')
    let s:renumLine=strpart(getline('.'),l:valuePos)
    call setline('.','')
  endif
  echo 'Renum line: '.s:renumLine

endfunction

" ==============================================================
" #include {{{1

function! SBimInclude()

  " Execute all '#include' directives.

  " Syntax:
  " #include filename

  " Warning: nested including is possible, but no recursion check is made!

  call cursor(1,1) " Go to the top of the file.
  let l:includedFiles=0 " Counter
  while search('^\s*#include\s\+','Wc')
    let l:includedFiles += 1
    let l:filename=matchstr(getline('.'),'\S\+.*',8)
    call setline('.',"' <<< start of included file ".l:filename)
    call append('.',"' >>> end of included file ".l:filename)
    let l:filecontent=readfile(s:sourceFileDir.'/'.l:filename)
    call append('.',l:filecontent)
  endwhile

  if l:includedFiles==0
    echo 'No file included'
  elseif l:includedFiles==1
    echo 'One file included'
  else
    echo l:includedFiles 'files included'
  endif

endfunction

" ==============================================================
" Conditional conversion {{{1

function! SBimDefine()

  " Search and execute all '#define' directives.

  " There can be any number of '#define' directives, but they
  " must be alone on their own source lines (with optional
  " indentation).

  call cursor(1,1) " Go to the top of the file
  while search('^\s*#define\s\+','Wc')
    let l:definition=getline('.')
    let l:tagPos=matchend(l:definition,'^\s*#define\s\+')
    let l:tag=strpart(l:definition,l:tagPos)
    if !empty(l:tag)
      call add(s:definedTags,l:tag)
    endif
    call setline('.','')
  endwhile

  let l:tags=len(s:definedTags)
  if l:tags==1
    echo l:tags.' #define directive'
  elseif l:tags>1
    echo l:tags.' #define directives'
  endif

endfunction

function! SBimDefined(needle)

  " Is _needle_ a defined tag?

"  echo "XXX About to search for the <".a:needle."> tag!"
  let l:found=0 " XXX needed, but why? Otherwise, error: undefined variable
  for l:tag in s:definedTags
"      echo 'XXX tag: '.l:tag
      let l:found=(l:tag==a:needle)
      if l:found
          break
      endif
  endfor
  return l:found

endfunction

function! SBimConditionalConversion()

  " Parse and interpret all conditional conversion directives.

  " XXX TODO -- Make the structure nestable.

  " Syntax:
  "
  "   #if[n]def tag
  "     ...
  "   #else
  "     ...
  "   #endif

  " Note: The conditions can not be nested.

  call cursor(1,1)

  let l:unresolvedCondition=0 " flag

  while search('^\s*#if\(n\)\?def\s\+.\+$','Wc')

    let l:else=0 " flag

"    echo 'XXX first #if[n]def found'

    while line('.')<line('$') " not at the end of the file?

      let l:currentLine=getline('.')

      if l:currentLine=~'^\s*#ifdef\s\+.\+'
        " #IFDEF
"        echo 'XXX #ifdef found'
        if l:unresolvedCondition
          echoerr '`#if[n]def` structures can not be nested'
          break
        else
          call SBimIfdef()
          let l:unresolvedCondition=1
        endif
      elseif l:currentLine=~'^\s*#ifndef\s\+.\+'
        " #IFNDEF
"        echo 'XXX #ifndef found ----------------------'
        if l:unresolvedCondition
          echoerr '`#ifndef` structures can not be nested'
          break
        else
          call SBimIfdef()
          let l:unresolvedCondition=1
        endif
"      elseif l:currentLine=~'^\s*#elseifdef\s\+.\+'
"        " #ELSEIFDEF
"        call setline('.','')
"        if !l:unresolvedCondition
"          " XXX TODO
"        endif
"      elseif l:currentLine=~'^\s*#elseifndef\s\+.\+'
"        " #ELSEIFNDEF
"        call setline('.','')
"        if !l:unresolvedCondition
"          " XXX TODO
"        endif
      elseif l:currentLine=~'^\s*#else\s*$'
        " #ELSE
"        echo 'XXX #else found'
        if l:else
          echoerr 'More than one `#else` in a `#if[n]def` structure'
          break
        else
          let l:else=1
          call setline('.','')
          let s:keepSource=!s:keepSource
        endif
      elseif l:currentLine=~'^\s*#endif\s*$'
        " #ENDIF
"        echo 'XXX #endif found'
        call setline('.','')
        let l:unresolvedCondition=0
        break
      else
        if l:unresolvedCondition && !s:keepSource
            call setline('.','')
        endif
      endif

      call cursor(line('.')+1,1) " go to the next line

    endwhile

    if l:unresolvedCondition
      echoerr '`#if[n]def` or `#endif` at line '.l:ifLineNumber
    endif

  endwhile

  echo 'Conditional conversion done'

endfunction

function! SBimIfdef()

    let l:ifLineNumber=line('.')
    let l:tagPos=matchend(getline('.'),'^\s*#if\(n\)\?def\s\+')
    let l:tag=Trim(strpart(getline('.'),l:tagPos))
"    echo 'XXX l:tag='.l:tag
    let l:tagMustBeDefined=(getline('.')=~'^\s*#ifdef')
"    echo 'XXX l:tagMustBeDefined='.l:tagMustBeDefined
    let l:tagIsDefined=SBimDefined(l:tag)
"    echo 'XXX l:tagIsDefined='.l:tagIsDefined
    let s:keepSource=(l:tagMustBeDefined && l:tagIsDefined) || (!l:tagMustBeDefined && !l:tagIsDefined)
"    echo 'XXX s:keepSource='.s:keepSource
    call setline('.','')

endfunction

" ==============================================================
" Labels

function! SBimLabels()

  " Translate the line labels.

  " Join lonely labels to the next line:
  silent! %substitute,^\s*\(\(label\s\+\)\?@[0-9a-zA-Z_]\+\)\s*\n,\1:,ei

  " Create an empty dictionary to store the line numbers of the
  " labels; the labels will be used as keys:
  let l:lineNumber={}
  " Go to the top of the file:
  normal gg
  " Store every label into the dictionary:
  while search('\(^\|:\)\s*\(label\s\+\)\?@[0-9a-zA-Z_]\+\>','Wc')
    " Store the label in the 'l' register:
    normal "l2yw
    " Debug message:
    " echo 'Raw label found: <' . getreg('l',1) . '>'
    " If the label is the second one in a line, remove the semicolon:
"     if stridx(getreg('l',1),':')==0
"      call setreg('l',strpart(getreg('l',1),1))
"    endif
    " If 'label' is present, go to the next word and repeat:
    if tolower(getreg('l',1))=='label @'
      normal w"l2yw
      " Debug message:
      " echo 'Actual raw label found: <' . getreg('l',1) . '>'
    endif
    " Remove possible ending spaces:
    let l:label=tolower(substitute(getreg('l',1),' ','','g'))
    " Debug message:
    "echo 'Clean label: <' . l:label . '>'
    " Use the label as the key to store the line number:
    let l:lineNumber[l:label]=line('.')+s:renumLine-1
    " Go to the next word:
    normal w
  endwhile

  " Debug message:
  "echo l:lineNumber

  " Remove the label definitions:
  silent! %substitute/^\s*\(label\s\+\)\?@[0-9a-zA-Z_]\+\s*:\?\s*//ei

  " Substitute every label reference with its line number:
  for l:label in keys(l:lineNumber)
    " Go to the top of the file:
    normal gg
    " Debug message:
    "echo 'About to translate label '.l:label
    " Do the subtitution:
    while search(l:label,'W')
      " Debug message:
      "echo 'Label '.l:label.' found'
      "execute "silent! substitute,".l:label."\\>,".l:lineNumber[l:label].","
      silent! execute "substitute,".l:label."\\>,".l:lineNumber[l:label].","
      " Debug message:
      "echo l:label.' --> '.l:lineNumber[l:label]
    endwhile
  endfor

  echo 'Labels translated.'

endfunction

" ==============================================================
" Output file

function! SBimOutputFile(outputFile)

  if empty(a:outputFile)
    let l:outputFile=expand('%').'_bas'
  else
    let l:outputFile=a:outputFile
  endif

  " Filename of the source file, without path
  "let s:sourceFilename=fnamemodify(expand('%'),':t')

  " Absolute directory of the source file
  let s:sourceFileDir=fnamemodify(expand('%'),':p:h')

  silent update " Write the current SBim file if needed
  split " Split the window

  " Save a copy of the input file as output file:
  silent execute 'write! '.l:outputFile

  " Open the output file for editing:
  silent execute 'edit '.l:outputFile
  set fileencoding=latin1
  set fileformat=unix " Force NL (char 10) as end of line

  echo 'S*BASIC file created.'

endfunction

" ==============================================================
" Main

function! SBim(outputFile)

  let s:ignoreCaseBackup=&ignorecase
  set ignorecase

  let s:definedTags=[] " a list for the '#define' tags

  call SBimOutputFile(a:outputFile)
  call SBimInclude()
  call SBimGetRenumLine()
  call SBimDefine()
  call SBimConditionalConversion()
  call SBimClean()
  call SBimLabels()
  call SBimRenum()

  silent w
  silent bw
  echo 'S*BASIC file saved and closed.'

  if s:ignoreCaseBackup
    set ignorecase
  else
    set noignorecase
  endif

  echo 'Done!'

endfunction

" Shortkey '_bas' in normal mode to create an S*BASIC file:
nmap <silent> _bas :call SBim("")<CR>

" ==============================================================
" Change log

" 2011-08-13: First version, named sb2sbasic.vim, based on
" vim2mb.vim ("Vim to MasterBASIC"), by the same author. [vim2mb
" was the origin of MBim].
"
" 2011-08-16: Added removing of blanks (tabs or spaces) at the
" end of lines; added start line parameter for renumbering;
" created alternative shortcut key to create boot files.
"
" 2011-08-25: Added C style comments, block and inline.
"
" 2011-09-01: Renamed to sbim2bas.vim; own renum function
" instead of using that of line_numbers.vim; all functions
" renamed with the "SBim" prefix.
"
" 2011-09-26: Renamed to sbim2sb.vim; some functions renamed;
" some comments improved.
"
" 2011-10-04: Block comments fixed with '\{-}'; 'silent' added
" to many commands; 'e' flag added to substitutions; custom
" messages added.
"
" 2012-01-25: New function SBimLabels(), based on BBimLabels()
" from BBim.  Fixed: lonely labels had to be joined with the
" next line; optional final ":" removed with labels.
"
" 2012-01-26: The character to split lines now is backslash
" instead of the vertical bar.
"
" 2012-01-28: Improvement: labels are not case sensitive any
" more.
"
" 2012-01-28: New feature: #firstline command to define the
" first line that will be used to renumber the final program in
" S*BASIC.
"
" 2012-01-28: Improvement: The S*BASIC buffer is actually closed
" with "bw" ("wq" kept it on the list).
"
" 2012-01-29: Improvement: Only one regex, simpler and fixed, to
" remove empty lines (the old three regex left the first line of
" the file blank).
"
" 2012-01-29: Fixed: Spaces or tabs at the end of line, after
" the split bar, are ignored.
"
" 2012-02-01: Bug found. The label translation loop doesn't end
" in some cases.
"
" 2012-02-15: Fixed? The 'c' parameter was removed from search()
" in the label translation loop.
"
" 2015-12-26: Changed the layout of the source. Renamed the
" program to "SBim".  Changed the license. 
"
" 2016-01-13: Fixed `SBimGetFirstLine()`: the directive was
" yanked instead of the line number.
"
" 2016-01-19: Typo. Updated header.
"
" 2016-01-25: Added `#include` directive.
"
" 2017-09-12: Make the output file configurable. Improve the
" line comments with one mandatory sourrounding space.  Allow
" Bash-style line comments at the end of the line. Add
" BASIC-style line comments with a single quote.
"
" 2017-09-20: Remove support for C-style and Bash-style
" comments. Fix the call in the map (missing parameter).
"
" 2017-09-21: Replace the C-style comment added by
" `SBimInclude()` with the new BASIC-style comment.
"
" 2017-09-28: Add a check to prevent the code from being loaded
" twice.
"
" 2017-09-29: Prepare implementation of conditional conversion
" (`#define`, `#ifdef`, `#ifndef`) -- code copied from
" Imbastardizer.
"
" 2017-10-17: Fix and tidy Imbastardizer's code about
" conditional conversion.
"
" 2017-10-18: Rename `#firstline` to `#renum`, `s:firstLine` to
" `s:renumLine` and `GetFirstLine` to `GetRenumLine`. Improve
" function `GetRenumLine`.

" vim: textwidth=64:ts=2:sw=2:sts=2:et

Descargas

También se puede descargar la última versión 2.0.0 en desarrollo de SBim en GitHub.

Páginas relacionadas

SuperBASIC Vim syntax file
Fichero de Vim para colorear código en SuperBASIC o SBASIC.
MBim
Utilería para desarrollo cruzado en MasterBASIC con SimCoupe.
BBim
Utilería para escribir programas para ZX Spectrum en BBim (formato mejorado de Beta BASIC) con el editor Vim.

Enlaces externos relacionados